# API Maker Documentation : Build > The pages of https://docs.apimaker.dev in the order of the navigation, each with its title, its description and its URL. The code is in TypeScript : the Markdown of a page (its URL with .md) has every language. --- # All APIs at a Glance > Every API of API Maker on one page - the operations of a table under /api/schema and /api/gen (17 on MongoDB, 14 on SQL tables) with their method and URL, the custom and system APIs, the query params and headers they share, and how to call them from your code. Source: https://docs.apimaker.dev/v4/docs/apis-all/overview.html Everything API Maker serves sits behind one base URL and the same token header. This page is the map ; every operation has its own page with examples. This page opens the **Build** tab : the APIs, then [your code](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html), its [languages](https://docs.apimaker.dev/v4/docs/languages/index.html), [schema and data](https://docs.apimaker.dev/v4/docs/schema/schema.html) and [SSR websites](https://docs.apimaker.dev/v4/docs/ssr/index.html). Running them is in the [Operate](https://docs.apimaker.dev/v4/docs/guides/index.html) tab. > Diagram : The API families : one base URL, one token, four HTTP families, your code in any language and the code API Maker runs for you > > The API families of API Maker on one base URL : schema APIs under /api/schema, generated APIs under /api/gen, custom APIs under /api/custom-api, system APIs under /api/system-api, the code of yours in nine runtimes, and the code API Maker runs on its own : events, schedulers, WebSocket events, process initializers and migrations. ## The URL of a table > Diagram : Anatomy of a database API URL : /api/schema/admin/mysql8/inventory/customers/get-by-id/42 > > Anatomy of a database API URL : /api, then the family schema or gen, then the user path of the account, the instance, the database, the table, and the operation with its parameters. | Segment | Meaning | |---|---| | `/api/schema` or `/api/gen` | The family : schema APIs apply the schema of the table, generated APIs are schemaless. | | `user-path` | The **API path** of the admin or developer account which owns the instance (`admin` for the first admin). It is shown on the profile of the account. | | `instance` | The name of the instance (the database server) in API Maker. For a tenant of a [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) instance : `instance::tenant`. | | `database` | The database on that server. | | `table` | The table or collection. | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## Instances, databases and tables `APIs & Logic → Instance API (DB API)` has four panels, left to right : instances, databases, tables, APIs. Pick one in each and the next panel fills. - **Instances** : a connection to a database server (MongoDB, MySQL with TiDB and Percona, MariaDB, SQL Server, PostgreSQL, Oracle). Its name is part of every URL and can not change later. Its connection string is a key of the [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html). - **Databases** and **tables** : what the connection sees, with their [settings](https://docs.apimaker.dev/v4/docs/settings/databaseSettings.html) and [schemas](https://docs.apimaker.dev/v4/docs/schema/schema.html). - **APIs** : the operations below, each with its [settings](https://docs.apimaker.dev/v4/docs/settings/apiSettings.html) and **Test API**. Every button of these panels is on the [table designer](https://docs.apimaker.dev/v4/docs/schema/table-designer.html#panels) page : adding an instance, making, editing and deleting databases and tables, and keeping the schemas in step. ## The operations of a table 17 on a MongoDB collection, 14 on a table of the SQL databases. The paths below follow `/api/schema////` (and the same under `/api/gen`). `[/:primaryKey]` names another column to use as the key. | Operation | Method and path | Body | Page | |---|---|---|---| | Get all | GET `/` | query params | [Get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.html) | | Get all by stream | GET `/stream` | query params | [Get all by stream](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.html) | | Get by id | GET `/get-by-id/:id[/:primaryKey]` | | [Get by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-by-id-api.html) | | Save single or multiple | POST `/save-single-or-multiple` | one object or an array | [Save single or multiple](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-save-single-or-multiple-api.html) | | Master save | POST `/master-save` | objects with nested related objects | [Master save](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-master-save-api.html) | | Update by id | PUT `/update-by-id/:id[/:primaryKey]` | the fields to change | [Update by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.html) | | Update many | PUT `/update-many` | `{ find, updateData }` | [Update many](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-many-api.html) | | Replace by id (MongoDB) | PUT `/replace-by-id/:id[/:primaryKey]` | the whole document | [Replace by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-replace-by-id-api.html) | | Array operations (MongoDB) | PUT `/array-operations` | `{ find, operations }` | [Array operations](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-array-operations-api.html) | | Remove by id | DELETE `/:id[/:primaryKey]` | | [Remove by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-id-api.html) | | Remove by query | POST `/query/delete` | `{ find }` | [Remove by query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-query-api.html) | | Query for get data | POST `/query` | `{ find, select, sort, skip, limit, deep, getTotalCount }` | [Query for get data](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-api.html) | | Query for get data by stream | POST `/query-stream` | same as query | [Query for get data by stream](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-by-stream-api.html) | | Aggregate (MongoDB) | POST `/aggregate` | a pipeline array | [Aggregate](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-aggregate-api.html) | | Count | POST `/count` | `{ find }` | [Count](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-count-api.html) | | Distinct | GET `/distinct/:field[/:order]` | | [Distinct](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-api.html) | | Distinct with query | POST `/distinct/:field[/:order]` | `{ find }` | [Distinct with query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-with-query-api.html) | - **MongoDB** gets all 17. **MySQL, MariaDB, SQL Server, PostgreSQL, Oracle, TiDB and Percona** get 14 : replace by id, array operations and aggregate are MongoDB only. - The schema operations exist for a table only once it has an active [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html). The generated ones are always there. - Their id in settings, groups, hooks and WebSocket subscriptions is `SCHEMA_GET_ALL`, `GEN_POST_BULK_INSERT`… : the full list is under [API ids](https://docs.apimaker.dev/v4/docs/apis-all/overview.html#api-ids). ## Shared by every read | | | |---|---| | [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) | `find`, `skip`, `limit`, `sort`, `select`, `deep`, `getTotalCount`, and any column name as a filter. In the body for the query APIs. | | [Request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) | Tokens, `x-am-response-case`, `x-am-content-type-response`, `x-am-response-object-type`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`… | | [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) | `{ success, statusCode, data, totalCount, errors, warnings, logs, meta }` | ## Your own APIs | Family | Method and path | Page | |---|---|---| | Custom API | ANY `/api/custom-api//` : the method and path of its settings | [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html) | | Pre and post hooks | run around every API of the account | [Pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) · [Post hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) | | Events | emitted by an API hit or by code, with listeners | [Events](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.html) | | WebSocket events | pushed to subscribed clients | [WebSocket events](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html) | | Schedulers | intervals and cron | [Schedulers](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.html) | | Utility classes | shared code, `import * as x from 'utils/X'` | [Utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) | ## System APIs All under `/api/system-api//…`, all POST, and all available in code as `g.sys.system.*` or `g.sys.cache.*`. | Group | APIs | |---|---| | Security | [`/token`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html) · [`/encrypt-data`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-encrypt-data-api.html) · [`/decrypt-data`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-decrypt-data-api.html) · [`/hash-data`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-hash-data-api.html) · [`/get-secret-by-name`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-secret-by-name-api.html) | | Redis and cache | [`/get-redis-key`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-redis-key-api.html) · [`/set-redis-key`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.html) · [`/remove-redis-key`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-remove-redis-key-api.html) · [`/reset-redis-cache-db`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-db-api.html) · [`/reset-redis-cache-custom-apis`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.html) · [`/reset-redis-cache-system-apis`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-system-api.html) | | Databases | [`/get-table-meta`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-table-meta-api.html) · [`/create-indexes`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.html) · [`/drop-indexes`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-drop-indexes-api.html) · [`/get-indexes`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-indexes-api.html) · [`/multi-tenant-instance-updated`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-multi-tenant-instance-updated-api.html) · [`/execute-plain-query`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-execute-plain-query-api.html) · `executeQuery` from [code](https://docs.apimaker.dev/v4/examples/sys/system/executeQuery.html) | | Events | [`/emit-event`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-api.html) · [`/emit-event-ws`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-ws-api.html) | | Validation | [`/is-valid-data-for-table`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-table-api.html) · [`/is-valid-data-for-custom-api`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-custom-api.html) · [`/is-valid-connection-string`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-connection-string-api.html) | | Outside | [`/call-external-api`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html) | ## From your code Every API above is a method of the [global object `g`](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) : `g.sys.db.getAll`, `g.sys.db.gen.queryGen`, `g.sys.system.encrypt`, `g.sys.cache.setKey`… with the same headers and query params, and [an example for each one](https://docs.apimaker.dev/v4/examples/index.html). **TypeScript** ```typescript import * as T from 'types'; import * as db from 'db-interfaces'; async function main(g: T.IAMGlobal) { const products = await g.sys.db.getAll({ instance: 'mongodb', database: 'shop', collection: 'products', queryParams: { find: { status: 'ACTIVE' }, limit: 10, deep: [{ s_key: 'category' }] }, }); return products; } module.exports = main; ``` ## API ids The ids used in groups, settings, hooks, events and WebSocket subscriptions. Schema APIs - SCHEMA_GET_ALL - SCHEMA_GET_ALL_STREAM - SCHEMA_GET_BY_ID - SCHEMA_POST_BULK_INSERT - SCHEMA_MASTER_SAVE - SCHEMA_ARRAY_OPERATIONS - SCHEMA_UPDATE_MANY - SCHEMA_PUT_UPDATE_BY_ID - SCHEMA_PUT_REPLACE_BY_ID - SCHEMA_DEL_DELETE_BY_ID - SCHEMA_POST_QUERY - SCHEMA_POST_QUERY_STREAM - SCHEMA_POST_QUERY_DELETE - SCHEMA_POST_AGGREGATE - SCHEMA_POST_COUNT - SCHEMA_GET_DISTINCT - SCHEMA_POST_DISTINCT_QUERY Generated APIs - GEN_GET_ALL - GEN_GET_ALL_STREAM - GEN_GET_BY_ID - GEN_POST_BULK_INSERT - GEN_MASTER_SAVE - GEN_ARRAY_OPERATIONS - GEN_UPDATE_MANY - GEN_PUT_UPDATE_BY_ID - GEN_PUT_REPLACE_BY_ID - GEN_DEL_DELETE_BY_ID - GEN_POST_QUERY - GEN_POST_QUERY_STREAM - GEN_POST_QUERY_DELETE - GEN_POST_AGGREGATE - GEN_POST_COUNT - GEN_GET_DISTINCT - GEN_POST_DISTINCT_QUERY System APIs - EXECUTE_PLAIN_QUERY - ENCRYPT_DATA - DECRYPT_DATA - HASH_DATA - GET_TOKEN - CALL_EXTERNAL_API - GET_SECRET - GET_REDIS_KEY - SET_REDIS_KEY - REMOVE_REDIS_KEY - CUSTOM_USER_CACHING - RESET_REDIS_CACHE_DB - RESET_REDIS_CACHE_CUSTOM_APIS - RESET_REDIS_CACHE_SYSTEM_APIS - 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 (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/overview.md) --- # Response Format > What every API Maker API answers - the envelope with success, statusCode, data, totalCount, errors, warnings, logs and meta, the HTTP status codes, and how headers change the shape and the format of the reply. Source: https://docs.apimaker.dev/v4/docs/apis-all/response-format.html Every API of API Maker, generated, schema, custom or system, answers with the same envelope, so one client handles all of them the same way. ## The envelope **A successful answer** ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 1, "first_name": "Bob", "last_name": "Lin" } ], "totalCount": 5, "warnings": [], "logs": [], "meta": {} } ``` - `data` holds the result, `totalCount` comes with `getTotalCount=true`, `warnings` and `logs` when there are some, `meta` with the header `x-am-meta: true`. Absent keys are simply not sent. **A refused request** ```json { "success": false, "statusCode": 400, "errors": [ { "type": "required", "field": "first_name", "message": "Please provide valid 'first_name' field with type 'string'.", "code": 400 } ], "warnings": [] } ``` - `errors` lists every problem found, each with its `code`, `message`, and for data problems the `field` and the `type` of the rule. The HTTP status of the reply is `statusCode`. | Key | When | Meaning | |---|---|---| | `success` | always | `true` when the request did what it was asked, `false` otherwise. | | `statusCode` | always | The HTTP status of the reply, repeated in the body : `200`, `201` for a save, `400`, `401`, `403`, `404` or `500`. | | `data` | on success, and on some failures | The result : an array for get all and the query APIs, an object for get by id, a save of one object, an update ; whatever a custom API returns. `null` when there is nothing. | | `totalCount` | with `getTotalCount=true` | The number of rows matching the filter, without `skip` and `limit`. | | `errors` | on failure | One entry per problem. Data problems name the `field` and the `type` of the rule ; the `message` follows the language asked with `x-am-internationalization`. | | `warnings` | when there are some | Problems which did not stop the request, in the same shape as errors : for example a `deep` which found no target. | | `logs` | when your code logged | What `g.logger` printed while a custom API, a hook or an event ran, so the caller can see it. | | `meta` | with `x-am-meta: true` | `executionTime`, `apiAccessGroups` (which group granted the call) and `runBy` (which server, process and sandbox ran the code). | | `encryptedData` | with `x-am-get-encrypted-data` | The response encrypted with the transfer key of the secret, next to or instead of `data`. | ## An error entry ```json { "type": "min", "field": "price_cents", "message": "Please provide minimum '0' for 'price_cents' field.", "code": 400, "dataIndex": 2 } ``` | Key | Meaning | |---|---| | `type` | The rule which failed : `required`, `min`, `max`, `minLength`, `maxLength`, `email`, `enumValidation`, `invalidValue` (a wrong type, a thrown `validatorFun`, a concurrency version mismatch…), `schemaKeyNotFound` (a key the schema does not have), `schemaNotFound`, `unique`, `virtualFieldUsedInFind`. | | `field` | The field concerned, with dots for nested fields. | | `message` | The text, from the [message templates](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) or your own throw, translated when the caller asks for a language. | | `code` | The HTTP status of this problem. The `statusCode` of the reply is the highest one. | | `dataIndex` | For a save of several objects : the index of the object in the array. | | `stack`, `apiCallSequence` | Present for unexpected errors of your code, to help you debug : the stack as lines, and the chain of APIs which led there. | ## Status codes | Code | Meaning | |---|---| | `200` | Done. | | `201` | Created : a save through save single or multiple or master save. | | `400` | The request is wrong : validation errors, an unknown key, a bad query, a missing body. | | `401` | The token is missing, malformed or expired. Get a new one from the [token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html). | | `403` | The token is fine but no group of the API user grants this API, this table or this field ; or the origin of the browser is not allowed. Fix the group, do not sign the user out. | | `404` | The path does not exist : a typo in the URL or a wrong method, an instance name which is not there. Also the answer of update by id, replace by id and remove by id with `throwErrorIfRecordNotFound=true` when the id is unknown. | | `500` | An error of your code or of the database. The message and, for your code, the stack are in `errors`. | ## The format of the reply The body is JSON unless the request asks otherwise with [headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : | Header | Effect | |---|---| | `x-am-content-type-response` | `application/json` (default), `text/xml`, `text/yaml`, `text/plain`, `text/html`, `application/octet-stream`. The envelope is the same, only its encoding changes. | | `x-am-response-case` | The case of every key : `camelCase`, `snakeCase`, `pascalCase`, `paramCase`… ; any other value leaves the keys as they are (see [the values](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-response-case)). | | `x-am-response-object-type: make_flat` | Nested objects flattened with `_` between the levels : `state_id_country_id_country_name`. | | `x-am-meta: true` | Adds `meta`. | | `x-am-get-encrypted-data` | Adds or replaces `data` with `encryptedData`. | | `x-no-compression: true` | No gzip or brotli, whatever the size (replies bigger than `compressThreshold`, 51200 bytes by default, are compressed otherwise). | A custom API can also answer something other than the envelope : set `g.res.contentType` to a MIME type and return a string, or return a file, see [Content types](https://docs.apimaker.dev/v4/examples/res/res.html#content-types) and the [download example](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html#files). ## Response headers | Header | Meaning | |---|---| | `x-am-data-source` | `cache` when the reply came from Redis, `api` when the API ran. | | `Content-Encoding` | `gzip` or `br` when the reply was compressed. | ## In your code The same envelope is what `g.sys.db.*`, `g.sys.system.*` and `g.sys.cache.*` return when you ask for the full response with the second argument `true`. Without it, they return `data` and throw the errors. **TypeScript** ```typescript const full = await g.sys.db.getAll({ instance: 'mongodb', database: 'shop', collection: 'products' }, true); if (!full.success) g.logger.error(full.errors); const rows = full.data; ``` (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/response-format.md) --- # Request Headers > Every request header API Maker understands - the tokens, the case and format of the response, flat objects, metadata, caching, encryption of the payload and the reply, the language of the messages, the tenant, the sandbox and compression - with an example of each. Source: https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html Headers change how API Maker answers a call, without changing the URL. They work on every API : generated, schema, custom and system. The response carries a few headers of its own, listed at the end. ## All headers | Header | Values | What it does | |---|---|---| | [`x-am-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-authorization) | a token | The API user : which application calls. Needed unless the API is public. | | [`x-am-user-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-user-authorization) | a token | The person, from a DB token generator. | | [`x-aws-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-aws-authorization), [`x-google-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-google-authorization), [`x-azure-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-azure-authorization), [`x-custom-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-custom-authorization) | a token | The person, from AWS Cognito, Google, Azure AD or a custom provider. | | [`x-am-response-case`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-response-case) | `camelCase`, `snakeCase`… | The case of every key of the answer. | | [`x-am-content-type-response`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-content-type-response) | `application/json`, `text/xml`, `text/yaml`… | The format of the answer. | | [`x-am-response-object-type`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-response-object-type) | `no_action`, `make_flat` | Flatten nested objects. | | [`x-am-meta`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-meta) | `true`, `false` | Execution time, plan, groups and sandbox in `meta`. | | [`x-am-cache-control`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-cache-control) | `no_action`, `reset_cache` | Drop the cached answers of the API before this call. | | [`x-am-get-encrypted-data`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-get-encrypted-data) | `no_encryption`, `get_only_encryption`, `get_data_and_encryption` | Encrypt the answer. | | [`x-am-encrypted-payload`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-encrypted-payload) | `true` | The body is encrypted. | | [`x-am-internationalization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-internationalization) | the name of a language | The language of the messages. | | [`x-am-tenant-username`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-tenant-username) | a tenant | The tenant of a multi-tenant instance. | | [`x-am-sandbox-timeout`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-sandbox-timeout) | milliseconds | How long the code of the call may run. | | [`x-am-run-in-sandbox`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-run-in-sandbox) | `0`, `1`, `2`… | Which sandboxes may run the code. | | [`x-no-compression`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-no-compression), [`accept-encoding`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#accept-encoding) | `true` / `br`, `gzip`, `deflate`, `identity` | Compression of the answer. | ## x-am-authorization The token of an [API user](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) : the application calling. Its [groups](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) decide which APIs, tables and fields the call may reach. The token comes from the [token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html) and expires after `jwtOptions.expiresIn` seconds (72 hours by default, `expiresInSeconds` in the token request changes it). ```text x-am-authorization: eyJhbGciOiJIUzI1NiIsInR1BydlRrblJlcS ``` - Missing or invalid : `401`. Valid but no group grants the API : `403`. - Public APIs (`apiAccessType: IS_PUBLIC` in the [settings](https://docs.apimaker.dev/v4/docs/settings/apiSettings.html)) do not need it. - Read it in your code from `g.req.auth.authAMUser`. ## x-am-user-authorization The token of a **person**, a row of your own users table, made by a DB token generator ([auth provider](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html)) through the token API. It is required when the settings of the database, the table, the API or the custom API list that provider in `authProviders`. ```text x-am-user-authorization: eyJhbGciOiJIpXVCJBydlRrblJlcS ``` - Read it in your code from `g.req.auth.authAMDB` : the columns of the user selected by the generator, and the groups of the groups column. - A token made for a tenant works for that tenant only, see [Multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html#users-of-a-tenant). ## x-aws-authorization The access token of AWS Cognito, when an [AWS auth provider](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#aws) is configured. Read from `g.req.auth.authAWS`. ```text x-aws-authorization: eyJhbGciOInR5cCI6IkpXVCJ9 ``` ## x-google-authorization The id token of a Google sign-in, when a [Google auth provider](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#google) is configured. Read from `g.req.auth.authGoogle`. ```text x-google-authorization: eyJhbGciOiJIUzI1I6IkpXVCJ9 ``` ## x-azure-authorization The token of Azure Active Directory, when an [Azure auth provider](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#azure) is configured. Read from `g.req.auth.authAzure`. ```text x-azure-authorization: eyJhbGciOiJIUzI1NI6IkpXVCJ9 ``` ## x-custom-authorization The token of a [custom auth provider](https://docs.apimaker.dev/v4/examples/req/auth/authCustom.html) : your own generator and validator code. What the validator returns is in `g.req.auth.authCustom`. ```text x-custom-authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ``` ## x-am-response-case Changes the case of every key of the answer, nested objects and arrays included. Default : `noChange`. | Value | `first_name` becomes | |---|---| | `noChange` | `first_name` | | `camelCase` | `firstName` | | `capitalCase` | `First Name` | | `constantCase` | `FIRST_NAME` | | `dotCase` | `first.name` | | `headerCase` | `First-Name` | | `noCase` | `first name` | | `paramCase` | `first-name` | | `pascalCase` | `FirstName` | | `pathCase` | `first/name` | | `sentenceCase` | `First name` | | `snakeCase` | `first_name` | ```text x-am-response-case: camelCase ``` **Answer** ```json { "firstName": "JOHN", "lastName": "DOE" } ``` A value which is not in the table leaves the keys as they are. ## x-am-response-object-type Flattens nested objects, such as the ones [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html) produces, into one level with `_` between the names : handy for grids, CSV exports and spreadsheets. Default : `no_action`. **no_action** ```json { "id": 101, "state_id": { "id": 201, "country_id": { "id": 301, "country_name": "INDIA" }, "state_name": "GUJARAT" }, "city_name": "AHMEDABAD" } ``` **make_flat** ```json { "id": 101, "state_id_id": 201, "state_id_country_id_id": 301, "state_id_country_id_country_name": "INDIA", "state_id_state_name": "GUJARAT", "city_name": "AHMEDABAD" } ``` ## x-am-meta Adds `meta` to the answer. Default : `false`. **x-am-meta: true** ```json { "data": [ { "id": 301, "country_name": "INDIA" } ], "meta": { "executionTime": "15ms", "executionTimeMS": 15, "apiAccessGroups": [ { "groupId": "6381b80359bdbd3a87c9abd5", "groupName": "all_permission", "hasAccess": true } ], "runBy": [ { "apiCategory": "CUSTOM_APIS", "serverId": "…", "serverName": "primary", "processId": "…", "port": 49731 } ] } } ``` | Key | Meaning | |---|---| | `executionTime`, `executionTimeMS` | Time spent in API Maker, without the network. | | `apiAccessGroups` | The groups of the API user and which one granted the call. | | `runBy` | Which server and process answered, and the port of the sandbox which ran the code : for calls which run code (custom APIs, hooks). | ## x-am-internationalization The name of a language of [i18N Management](https://docs.apimaker.dev/v4/docs/i18/i18.html). Every message of the answer, the messages of API Maker as well as your own from `errorList` and thrown strings, comes back in that language. The value `Default` (or no header) gives the original messages. ```text x-am-internationalization: Hindi ``` **Default** ```json { "code": 400, "message": "Please provide id param value" } ``` **Hindi** ```json { "code": 400, "message": "कृपया आईडी पैरामीटर का मान प्रदान करें." } ``` **Chinese simple** ```json { "code": 400, "message": "请提供id参数的值" } ``` **Spanish** ```json { "code": 400, "message": "Proporcione el valor del parámetro id." } ``` **Japanese** ```json { "code": 400, "message": "id パラメータの値を入力してください" } ``` **Urdu** ```json { "code": 400, "message": "براہ کرم id پیرامیٹر کی قدر فراہم کریں۔" } ``` - Add a language, change a message, and the next call answers with it : no restart. ## x-am-run-in-sandbox Which of the sandboxes of the account may run the code of this call (custom API, hooks, events). An account has one sandbox of a language on each server by default, for the whole API Maker process, and up to `sandboxCountForAdmin` while it is busy. ```text x-am-run-in-sandbox: 0 # any sandbox (default) x-am-run-in-sandbox: 1 # always the first sandbox x-am-run-in-sandbox: 2 # one of the first two x-am-run-in-sandbox: 3 # one of the first three ``` - Pin a call to one sandbox when your code keeps state in memory, such as a connection opened by a [process initializer](https://docs.apimaker.dev/v4/docs/features/process-initializers.html). ## x-am-content-type-response The format of the answer. Default : `application/json`. The envelope is the same in every format. | Value | The answer | |---|---| | `application/json` | `{ "success": true, "statusCode": 200, "data": [ … ] }` | | `text/xml` | The same envelope in XML, an array as `<_el>` items. | | `text/yaml` | The same envelope in YAML. | | `text/plain`, `text/html`, `application/octet-stream` | The same envelope as text, HTML or bytes. | **text/xml** ```xml true 200 <_el> 4 JOHNNY ``` A custom API can also decide the format itself with `g.res.contentType`, see [Content types](https://docs.apimaker.dev/v4/examples/res/res.html#content-types). ## x-am-cache-control For an API with [caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) on. Default : `no_action`. | Value | What happens | `x-am-data-source` of the answer | |---|---|---| | `no_action` | The answer comes from Redis when it is there. | `cache` | | `reset_cache` | The API runs against the database, and its answer replaces the cached one. | `api` | ## x-am-get-encrypted-data Encrypts the answer with `encryptionAlgorithmFETransfer` and `secretFETransfer` of the secret, the key you share with your frontend or mobile app. Default : `no_encryption`. | Value | The answer | |---|---| | `no_encryption` | `data` only. | | `get_only_encryption` | `encryptedData`, and `data` is `null`. | | `get_data_and_encryption` | `data` and `encryptedData`. | **get_only_encryption** ```json { "success": true, "statusCode": 200, "data": null, "encryptedData": "U2FsdGVkX1+6qMdb3jXwJAUWH/qlwBq75mtA1kzpWccrNZRAr+CE2c3VtGpkEtVjH==" } ``` - The client decrypts `encryptedData` with the same algorithm and key (AES, or a legacy RC4 or TripleDES, as in the secret). Anyone who has the app has that key : see [what it protects](https://docs.apimaker.dev/v4/docs/features/security-features.html#encrypted-request-payloads). ## x-am-encrypted-payload Tells API Maker that the body is encrypted : `{ "dataEncFE": "…" }`, where `dataEncFE` is `{ data, createdAt }` encrypted with the transfer key of the secret. A payload older than `feTransferDataValidityInSeconds` is refused. With `acceptOnlyEncryptedData: true` in the settings, plain bodies are refused. See [Security features](https://docs.apimaker.dev/v4/docs/features/security-features.html#encrypted-request-payloads). ```text x-am-encrypted-payload: true ``` ## x-am-tenant-username The tenant of the request, for the APIs of a [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) instance : they work on the database of that tenant. - It names the tenant only. Each multi-tenant instance looks the tenant up in its own tenants table, the entry of the default secret chosen as its **Connection String Multi Tenant**. So one header serves every multi-tenant instance a request or a custom API uses, even when they use different tenants tables. - It does what naming the tenant in the path does, after the instance name and two colons : `/api/schema/admin/crm::acme/crm/customers`. When a request does both, the tenant of the path is used. - A tenant the tenants table does not have, or which the `find` of its secret entry leaves out, is refused with 400 : `Unable to find tenant with username 'acme'.` - An empty value names no tenant : the request works on the structure database. - A custom API reads the tenant in `g.req.params.tenantUsername`, and passes it on to the APIs it calls with `g.sys.db` and `g.sys.system`. The hooks of the request do the same. - Custom and system APIs with caching keep their cached answers per tenant. - A [token of a user](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html#users-of-a-tenant) made with this header works for that tenant only. - An instance which is not multi-tenant has one database for every tenant : the header does not change the database it uses. ```text GET /api/schema/admin/crm/crm/customers x-am-tenant-username: acme ``` ## x-am-sandbox-timeout How long the code of this call (a custom API and the hooks of its versions, the hooks of an API) may run, in milliseconds, wherever it runs : in a runner of a sandbox, in the runner of the server (`runOnNativeProcess`), or in the server itself. Default : the time of the custom API (`customApiTimeoutInSeconds`), else `sandboxReqTimeout` of the [configuration](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html), `13000`. When the time is over, the answer is the error `API didn't respond in ms.`, and the runner which holds the code takes no more requests : it is ended once the other requests it runs are over, each in its own time. ```text x-am-sandbox-timeout: 60000 ``` - A custom API sets its own limit in its settings with `customApiTimeoutInSeconds` : the header wins over it. - Event listeners, schedulers, migration scripts and process initializers run with the time each of them names : see [Time of a run](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#time-of-a-run). ## x-no-compression Answers of at least `compressThreshold` bytes (51200 by default) are compressed. `true` sends this answer as it is. - A file sent by a custom API is compressed whatever its size. Streamed answers (the `/stream` APIs) are never compressed. ```text x-no-compression: true ``` ## accept-encoding The standard header. API Maker picks the first of `gzip`, `br` (Brotli) and `deflate` which the header accepts : a browser sending `gzip, deflate, br` gets gzip, which costs far less CPU than Brotli for nearly the same size. Send `accept-encoding: br` to get Brotli. `identity`, or no header at all, means no compression. ```text accept-encoding: gzip ``` ## Response headers | Header | Meaning | |---|---| | `x-am-data-source` | `cache` when the answer came from Redis, `api` when the API ran. Always `api` for streams. | | `x-am-request-id` | The id of the request, the same in the logs of API Maker. | | `content-encoding` | `br`, `gzip` or `deflate` when the answer is compressed. | ## From your code The same headers go in `headers` of every `g.sys` call : **TypeScript** ```typescript const rows = await g.sys.db.getAll({ instance: 'mysql8', database: 'inventory', collection: 'customers', headers: { 'x-am-response-case': 'camelCase', 'x-am-tenant-username': 'acme' }, }); ``` (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.md) --- # Error Messages Reference > Every message template of API Maker (AM_00001 to AM_00125) with its meaning, what triggers it and how to fix it - validation, deep populate, tokens, secrets, encryption, multi-tenant, files and more. Each one can be translated per caller with internationalization. Source: https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html Every message API Maker answers comes from a template with a code. The codes matter for two things : you recognise a problem quickly, and you can translate any template per caller with [internationalization](https://docs.apimaker.dev/v4/docs/i18/i18.html) (the key `x-am-internationalization`). The `{field}`, `{value}`… placeholders are filled with the details of the request. ## Data and schema | Code | Message | When | |---|---|---| | AM_00001 | Please provide valid '{field}' field with type '{type}'. | A required field is missing or has the wrong type. Also the version field of [optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.html) missing on an update. | | AM_00002 | Please provide minimum '{value}' for '{field}' field. | `validations.min` | | AM_00003 | Please provide maximum '{value}' for '{field}' field. | `validations.max` | | AM_00004 | Property '{field}' should have minimum length of '{value}'. | `validations.minLength` | | AM_00005 | Property '{field}' should have maximum length of '{value}'. | `validations.maxLength` | | AM_00006 | Please provide unique value '{value}' for key '{field}' in data index '{dataIndex}'. | The same value twice in one save for a field with `validations.unique`. | | AM_00007, AM_00008 | Please provide unique combination for keys ['{validationPath}'] in data index '{dataIndex}'. | The same combination twice in one save. | | AM_00009, AM_00020 | {field} should be array as it is logical operator. | `$and`, `$or`, `$nor` with something else than an array. | | AM_00010 | {field} should be array in schema as data is in array. | An array sent for a field the schema declares as a single value. | | AM_00011, AM_00013, AM_00014 | '{value}' is not a valid number for key '{field}'. | A value which can not become the number the schema asks. | | AM_00015, AM_00016 | Invalid date in '{field}' field. | A value which can not become a date. | | AM_00017, AM_00018, AM_00019 | Invalid objectId in '{field}' field. | An object, an array or a bad string for an objectId field. | | AM_00021, AM_00022 | Invalid date in min / max field in schema validation for '{field}' field. | The schema itself has a bad `min` or `max` date. | | AM_00023, AM_00024 | Array is expected in key '{field}'. | `$in` and `$nin` need an array, and not an empty one for some operators. | | AM_00058 | Found empty schema to validate. | A validation asked with no schema. | | AM_00061 | Invalid data found for saving. Please provide data in JSON object format to save. | The body of a save is not an object or an array of objects. | | AM_00062, AM_00112 | Type of '{field}' should be array in schema. | Array operations or an array value on a field which is not an array in the schema. | | AM_00087 | Please provide valid value in {field}. | A `validatorFun` returned a falsy value. A thrown error uses its own message instead. | | AM_00101 | {message} | Any message thrown by your code (`validatorFun`, `conversionFun`, custom API), passed through i18n. | | AM_00111 | Property '{field}' should be valid email address. | `validations.email` | | AM_00114 | [{field}] is duplicate having value [{value}]. | A duplicate the database refused (unique index). | | AM_00116 | Property '{field}' should have any value from {value}. | `validations.enum` | | AM_00117 | Concurrency version mismatch in '{field}'. This row/document is already updated. | The version sent is not the one of the row : [optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.html). | | AM_00118 | Virtual field can not be used in find join feature. '{field}' is virtual field. | A `find` on a virtual field. | | AM_00119 | Record not found. | `throwErrorIfRecordNotFound=true` and no row for the id. | ## Query, deep populate and find join | Code | Message | When | |---|---|---| | AM_00012 | Please provide id param value | Get by id, update by id, remove by id without an id. | | AM_00036 | Please provide t_col value. | A `deep` item without `t_col`, and no relation in the schema for its `s_key`. | | AM_00037 | Unable to find collection for s_key:'{field}', You can provide it in schema or deep by 't_col' property. | Same, for a nested level. | | AM_00039 | Unable to find collection with name '{collectionName}' on path '{instanceName} -> {databaseName} -> {collectionName}'. | A `t_col` which does not exist. | | AM_00040 | Unable to find primary key for target collection '{collectionName}'… | The target has no primary key and the deep item has no `t_key`. | | AM_00044 | Please provide array for aggregate body. | The aggregate API needs a pipeline array. | | AM_00045, AM_00047 | Please provide field for distinct API. You can also provide multiple fields separated by comma. | Distinct without a field. | | AM_00046, AM_00048 | Invalid column ['{field}'] found in your field list. | `select` or distinct with a column the schema does not have. | | AM_00049 | Please provide 'find' property having query to remove data. | Remove by query without `find` : nothing is ever deleted without a filter. | | AM_00051 | Primary key '{field}' not found in schema. | The `primaryKey` param names a column which is not in the schema. | | AM_00052, AM_00053 | Unable to convert '{value}' to number / objectId based on your schema type. | An id which does not fit the type of the primary key. | | AM_00054, AM_00055 | Invalid primary key type '{type}' found for '{field}'. / Primary key not found in '{instanceName}' -> '{databaseName}' -> '{collectionName}'. | The table has no usable primary key. | | AM_00056 | Schema is not defined for this collection '{collectionName}'. You can define schema or please try /api/gen version of the api. | A schema API called on a table without a schema. | | AM_00060 | find property should be string of valid JSON/JSON5. | A `find` query param which does not parse. | | AM_00063, AM_00066, AM_00067 | Unable to find schema / property '{field}' in schema of '{instanceName}' ➞ '{databaseName}' ➞ '{collectionName}'. | A `deep` or find join reaching a key or a table without a schema (schema APIs). | | AM_00085 | $in/$nin operator value must be non empty array. | `$in` or `$nin` with what is no array (SQL instances, and the conditions of `$and`, `$or` and `$nor`). An empty array is taken : `$in` of it matches no row, `$nin` every row. | | AM_00086 | $gt/$gte/$lt/$lte value should be valid [string, number, date (iso format)]. | | | AM_00110 | Multiple operators [{value}] are not supported in SQL databases at this moment. You can use $and operator to achieve that. | Not answered since v4 : several operators on one field are taken on every database. | | AM_00042, AM_00064, AM_00095 | This API is only supported for mongodb. / This API is not supported. / Replace by id API is not supported for instance type '{type}'. | Aggregate, array operations and replace by id on a SQL instance. | ## Tokens and access | Code | Message | When | |---|---|---| | AM_00034 | User '{name}' with username '{username}' is not having access to this API '{url}'. | No group of the API user grants the API : `403`. | | AM_00035 | Authorization header 'x-am-authorization' is not valid. | Bad or expired API user token : `401`. | | AM_00050 | User '{name}' does not have access to fields [{field}]. | A write to a field the group can not write. | | AM_00084, AM_00115 | Invalid access token. / Invalid JWT token provided. | A token which does not verify. | | AM_00090 | Please provide token request with ['u', 'p'] or ['refresh_token']. | The token API body is incomplete. | | AM_00092, AM_00093 | Please provide '{name}' token in request headers. / Invalid token provided in '{name}' header. | The table asks for a person token which is missing or invalid. | | AM_00094 | Token user not found in '{instanceName}' -> '{databaseName}' -> '{collectionName}'. | The person of the token is no longer in the users table. | | AM_00105 | Multiple entries exists for this username '{value}' in database collection|table. | The users table has the username twice. | | AM_00109 | Access groups not assigned to user. | The person has no group in the groups column. | | AM_00113 | You are not authorized to access this API. | Generic refusal. | | AM_00032 | Invalid request origin. You can give access to this host '{origin}' from settings. | The browser origin is not in the allowed origins of the [sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html). | ## Secrets, encryption and payloads | Code | Message | When | |---|---|---| | AM_00025 | A default secret is required because of encryption used in key '{field}'. | The account has no active default secret. | | AM_00026 | Unable to find valid algorithm named '{algorithm}' for '{field}' field. These are valid encryption algorithms ['AES', 'RC4', 'TRIPLEDES']. | | | AM_00027, AM_00029 | Unable to find encryption algorithm / secret for '{field}' field. You can provide its value in 'common.encryptionAlgorithm' & 'common.secret' key. | The default secret lacks the keys a field with `encryption: true` needs. | | AM_00028, AM_00030 | Unable to find hashing algorithm / nonce for '{field}' field. | Same for `hashing: true`. | | AM_00059 | Default secret is not present in the system. | Get secret was called and the account has no active default secret. | | AM_00103, AM_00104, AM_00106, AM_00107, AM_00108 | Unable to decrypt data of 'dataEncFE' field. / Payload expired. / Please provide field 'createdAt'… | An [encrypted payload](https://docs.apimaker.dev/v4/docs/features/security-features.html#encrypted-request-payloads) which does not decrypt, is too old, or lacks `createdAt`. | | AM_00068 | Invalid request. Please provide valid value for header x-am-code-hash. | | ## Instances, custom code and files | Code | Message | When | |---|---|---| | AM_00031 | Instance with name '{instanceName}' does not exist. | | | AM_00057, AM_00099 | Path '{url}' does not exist. / Please provide valid API path. Can not find any user with path '{value}'. | A wrong user path or custom API path : `404`. | | AM_00065 | Unsupported value found for parameter '{field}' | | | AM_00069, AM_00070, AM_00071, AM_00072 | Migration script … not found / Execution is locked … | [Database migrations](https://docs.apimaker.dev/v4/docs/features/database-migration.html) | | AM_00073, AM_00074 | Unable to find custom API with name '{name}'. / … version '{versionNumber}' … | A `g.sys.system.callExternalApi` or a test naming a custom API which is not there. | | AM_00075, AM_00076 | Unable to find scheduler … | | | AM_00077 | Unable to find version '{versionNumber}' in schema of … | | | AM_00081, AM_00082 | Unable to find utility class with name '{name}'. / … version … | An `import * as x from 'utils/X'` of a class which does not exist. | | AM_00096 | Unable to find system API with name '{name}'. | | | AM_00097, AM_00098 | Please provide 'path' property. / Please provide request body. | | | AM_00100 | WebSocket event not found with name '{name}'. You can create this event in API Info → WebSocket Events. | `emitEventWS` with an unknown event. | | AM_00120 | File upload is not supported in [{name}] API. | | | AM_00121, AM_00122 | Minimum / Maximum file size supported is '{value}' bytes and you provided [{name}] file with '{value2}' bytes in '{field}' field. | Custom API file uploads | | AM_00123 | Allowed extensions are [{value}] and you provided file '{name}' with extension '{value2}'. | | | AM_00124 | Allowed fields are [{value}] and you have provided value in field '{field}'. | | | AM_00125 | Please provide ['oracleDBUsername', 'oracleDBPassword', 'oracleDBPrivilege']. | An Oracle instance without its credentials. | ## Translating them Open **APIs & Logic → i18N Management**, create a language, and change the value of any code. Send the name of the language in `x-am-internationalization` and every message of the reply follows it. Your own messages (custom API `errorList`, thrown strings) can be mapped too. See [Internationalization](https://docs.apimaker.dev/v4/docs/i18/i18.html). --- # Query Parameters > The query params every read API of API Maker understands - find, skip, limit, sort, select, deep and getTotalCount, any column as a filter, upsert and returnDocument for updates - with examples, and the same keys in the body of the query APIs. Source: https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html The read APIs of a table ([get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.html), [get all by stream](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.html), [get by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-by-id-api.html), [distinct](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-api.html)) take their options as query params of the URL. The [query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-api.html) APIs take the same keys in their JSON body, and the write APIs accept `select` and `deep` to shape their answer. | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The params | Param | Example | What it does | |---|---|---| | [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html) | `?find={first_name:'Bob',pincode:{$gte:382345}}` | Filter with the MongoDB operators, on every database, including fields of related tables. | | any column | `?first_name=Bob,Alice&pincode>=382345` | A column name is a filter too, with `>`, `>=`, `<`, `<=`, `!=`, comma lists and regular expressions. | | [skip](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#skip) | `?skip=20` | Leave out the first rows. | | [limit](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#limit) | `?limit=10` | At most this many rows. Without it, every row. | | [sort](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#sort) | `?sort=-last_update,first_name` | Order, `-` for descending, several columns. | | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.html) | `?select=first_name,last_name` or `?select=-password` | Only these fields, or everything but these. | | [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html) | `?deep=[{s_key:'customer_id',t_col:'orders',t_key:'customer_id',isMultiple:true}]` | Replace ids with the related rows, from any table, database or instance, any depth. | | [getTotalCount](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#getTotalCount) | `?getTotalCount=true` | Add `totalCount` : the rows matching the filter before paging. | | [upsert](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#upsert) | `?upsert=true` | Update by id and replace by id : insert when the id does not exist. | | [returnDocument](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#returnDocument) | `?returnDocument=before` | Update by id and replace by id : answer with the row before the change. | | throwErrorIfRecordNotFound | `?throwErrorIfRecordNotFound=true` | Update, replace and remove by id : `404` instead of `data: null` for an unknown id. | ## skip Leaves out the first rows. With `limit`, it pages through a table : page 3 of 10 rows is `skip=20&limit=10`. ```text GET /api/schema/admin/mysql8/inventory/customers?skip=20&limit=10&sort=customer_id ``` - A whole number ; `0` or absent skips nothing. - Always add a `sort` when you page : without an order, the database may return the rows in any order, and a page may repeat or miss rows. ## limit At most this many rows. Without it, get all and the query APIs return every row which matches : give a `limit` as soon as a table can grow. ```text GET /api/schema/admin/mysql8/inventory/customers?limit=2&sort=customer_id ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 1, "first_name": "Bob", "last_name": "Lin" }, { "customer_id": 2, "first_name": "Alice", "last_name": "Page" } ] } ``` - A whole number greater than zero. - The streams ([get all by stream](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.html), query by stream) send big results without a limit and without holding them in memory. ## sort Orders the rows : name the columns, with `-` in front of a column for descending order. ```text GET /api/schema/admin/mysql8/inventory/customers?sort=-last_update,first_name ``` **In the body of the query APIs : a string or an object** ```json { "find": {}, "sort": "-last_update,first_name" } { "find": {}, "sort": { "last_update": -1, "first_name": 1 } } ``` - Spaces around the commas are fine. On MongoDB a nested field sorts too : `sort=address.city`. - Sorting a big table on a column without an index is slow : [Index Maker](https://docs.apimaker.dev/v4/extensions/index_maker/introduction.html) finds the queries which need one. ## getTotalCount `getTotalCount=true` adds `totalCount` to the answer : the rows matching the filter, before `skip` and `limit`. One call gives the page and the total. ```text GET /api/schema/admin/mysql8/inventory/customers?find={isActive:1}&skip=0&limit=2&getTotalCount=true ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 1, "first_name": "Bob", "last_name": "Lin" }, { "customer_id": 2, "first_name": "Alice", "last_name": "Page" } ], "totalCount": 5 } ``` - It works on get all, get all by stream, query and query by stream ; a stream sends `totalCount` after the rows. The [count](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-count-api.html) API gives the number alone. - The count is a second query on the database : on a very big table with a complex filter, it costs as much as the query itself. ## upsert `upsert=true` makes [update by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.html) and [replace by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-replace-by-id-api.html) insert a row when the id does not exist. Without it, an unknown id changes nothing and answers `data: null`. ```text PUT /api/schema/admin/mysql8/inventory/customers/update-by-id/999?upsert=true ``` **Body** ```json { "first_name": "John", "last_name": "Sina", "pincode": 382350, "isActive": 0 } ``` | Row 999 | What happens | Answer | |---|---|---| | exists | updated with the body | the row after the change | | does not exist | inserted with `customer_id: 999` and the body | the new row | - The inserted row is validated as a save : with a schema, every `required` field must be in the body. - From code : `g.sys.db.updateById({ ..., upsert: true })`. ## returnDocument Which version of the row an update by id or a replace by id sends back : `after` the change (the default), or `before` it. It changes the answer only, never what is written. ```text PUT /api/schema/admin/mysql8/inventory/customers/update-by-id/1?returnDocument=before ``` **Answer : the row before the change of pincode** ```json { "success": true, "statusCode": 200, "data": { "customer_id": 1, "first_name": "Bob", "last_name": "Lin", "pincode": 382345 } } ``` - `select` and `deep` apply to the version returned. With `upsert` and an insert, `before` answers `null`. - From code : `g.sys.db.updateById({ ..., returnDocument: 'before' })`. ## In a deep item `skip`, `limit` and `sort` inside a [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html) item apply to the related rows of each parent : the 5 latest orders of every customer, for example. ## Writing them - Values are **JSON5** : `find={first_name:'Bob'}` needs no quotes around keys and accepts single quotes. Strict JSON works too. - URL encode the value when your client does not : `find=%7Bfirst_name%3A'Bob'%7D`. The API testing page and `g.sys.db` do it for you. - Numbers, booleans and ISO dates in a column param are cast to their type before the query (`?pincode=382345` compares a number). - The same keys go in the body of the query APIs, as JSON : ```json { "find": { "first_name": "Bob" }, "sort": "-customer_id", "skip": 0, "limit": 10, "select": "first_name,last_name", "deep": [ { "s_key": "customer_id", "t_col": "orders", "t_key": "customer_id" } ], "getTotalCount": true } ``` ## From code `g.sys.db.getAll({ instance, database, collection, queryParams: { find: {…}, sort: '-customer_id', limit: 10 } })` : `queryParams` takes the same keys, as objects rather than strings. See the [code examples](https://docs.apimaker.dev/v4/examples/sys/db/getAll.html). ## Schema or generated The schema APIs convert the values of `find` to the types of the schema (`"8"` matches the number 8) and know the relations for `deep`. The generated APIs compare what you send as it is. See [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html). --- # find Parameter > Filter rows with the find parameter of API Maker - every query operator of MongoDB ($eq, $in, $and, $or, $nor, $not, $exists, $regex, $mod, $expr, $elemMatch...), translated to SQL for MySQL, MariaDB, PostgreSQL, SQL Server and Oracle, and find and join on fields of related tables. Source: https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html `find` filters the rows. It takes an object written as MongoDB writes a filter, with every query operator of MongoDB, and API Maker translates it for MySQL, MariaDB, PostgreSQL, SQL Server and Oracle as well, so one filter language serves every database. **As a query param (JSON5)** ```text GET /api/schema/admin/mysql8/inventory/customers?find={first_name:'Bob'} ``` **In the body of the query APIs** ```json { "find": { "first_name": "Bob" } } ``` | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## Operators On a MongoDB instance every operator is the one of MongoDB. The last column says what a SQL instance is asked : **MongoDB** there means that no table answers it, and API Maker refuses it on a SQL instance with `Unknown Operator`. ### Comparison | Operator | Example | Rows returned | SQL | |---|---|---|---| | a value | `{ "customer_id": 2 }` | equal to the value | `=` | | `$eq` | `{ "customer_id": { "$eq": 2 } }` | equal | `=` | | `$ne` | `{ "customer_id": { "$ne": 2 } }` | not equal | `!=` | | `$gt`, `$gte` | `{ "customer_id": { "$gt": 2 } }` | greater than, greater or equal | `>`, `>=` | | `$lt`, `$lte` | `{ "customer_id": { "$lte": 2 } }` | less than, less or equal | `<`, `<=` | | `$in` | `{ "customer_id": { "$in": [ 1, 2 ] } }` | one of the values | `IN (...)` | | `$nin` | `{ "customer_id": { "$nin": [ 2, 3, 4 ] } }` | none of the values | `NOT IN (...)` | - Two keys in one object are both required : `{ "first_name": "Eve", "pincode": 382349 }`. - Several operators on one field are all required, on every database : `{ "customer_id": { "$gte": 1, "$lte": 9 } }`. - Comparisons work on numbers, strings and dates in ISO format : `{ "last_update": { "$gte": "2022-11-01T00:00:00.000Z" } }`. - `$in` and `$nin` take an array. `$in` of an empty array returns no row, `$nin` of one every row. ### Logical | Operator | Example | Rows returned | SQL | |---|---|---|---| | `$and` | `{ "$and": [ { "customer_id": 1 }, { "pincode": 382345 } ] }` | every condition | `a AND b` | | `$or` | `{ "$or": [ { "customer_id": 1 }, { "pincode": 382345 } ] }` | any condition | `(a OR b)` | | `$nor` | `{ "$nor": [ { "customer_id": 1 }, { "pincode": 382345 } ] }` | none of the conditions | `NOT (a OR b)` | | `$not` | `{ "customer_id": { "$not": { "$gt": 3 } } }` | the opposite of the operators | `NOT (a > ?)`, `NOT IN`, `NOT LIKE` | `$and`, `$or` and `$nor` take an array of conditions, and go inside each other as deep as you need. Columns next to them are required too. **Columns, $or, $and and $nor together** ```json { "isActive": 1, "$or": [ { "first_name": "Eve", "pincode": { "$gte": 382348 } }, { "$and": [ { "last_name": "Page" }, { "$or": [ { "customer_id": 2 }, { "customer_id": { "$in": [ 7, 9 ] } } ] } ] } ], "$nor": [ { "first_name": "Mallory" }, { "pincode": { "$lt": 382345 } } ] } ``` **What a PostgreSQL instance is asked** ```sql "isActive" = $1 AND (("first_name" = $2 AND "pincode" >= $3) OR ("last_name" = $4 AND ("customer_id" = $5 OR "customer_id" IN ($6, $7)))) AND NOT ("first_name" = $8 OR "pincode" < $9) ``` ### Element | Operator | Example | Rows returned | SQL | |---|---|---|---| | `$exists` | `{ "last_update": { "$exists": true } }` | with the field, or without it (`false`) | `IS NOT NULL`, `IS NULL` | | `$type` | `{ "pincode": { "$type": "string" } }` | whose value is of a BSON type | MongoDB | ### Evaluation | Operator | Example | Rows returned | SQL | |---|---|---|---| | `$regex`, `$options` | `{ "first_name": { "$regex": "^ma", "$options": "i" } }` | matching the regular expression | [by database](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html#regular-expressions) | | `$mod` | `{ "customer_id": { "$mod": [ 2, 0 ] } }` | whose value divided by 2 leaves 0 | `MOD(a, 2) = 0`, `a % 2 = 0` | | `$expr` | `{ "$expr": { "$gt": [ "$spent", "$budget" ] } }` | for which the expression is true : it compares fields | [comparisons and arithmetic](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html#expr-on-a-sql-instance) | | `$text` | `{ "$text": { "$search": "coffee" } }` | found by the text index | MongoDB | | `$where` | `{ "$where": "this.a > this.b" }` | for which the JavaScript is true | MongoDB | | `$jsonSchema` | `{ "$jsonSchema": { "required": [ "name" ] } }` | valid against the schema | MongoDB | | `$comment` | `{ "$comment": "nightly report", "isActive": 1 }` | a note next to the conditions, it filters nothing | left out | ### Array A field of a MongoDB document can hold an array. A column of a SQL table holds one value, so these are for MongoDB instances. | Operator | Example | Rows returned | |---|---|---| | a value | `{ "tags": "red" }` | whose array holds the value | | an operator | `{ "scores": { "$gt": 80 } }` | whose array holds a value which meets it | | `$all` | `{ "tags": { "$all": [ "red", "blue" ] } }` | whose array holds every value | | `$elemMatch` | `{ "orders": { "$elemMatch": { "qty": { "$gt": 5 }, "status": "PAID" } } }` | with one item which meets every condition | | `$elemMatch` | `{ "scores": { "$elemMatch": { "$gt": 80, "$lt": 90 } } }` | with one value which meets every operator | | `$size` | `{ "tags": { "$size": 2 } }` | whose array has that many items | | a position | `{ "orders.0.qty": { "$gt": 5 } }` | whose first item meets the condition | `$all` takes `$elemMatch` items, and `$not` negates `$elemMatch` : `{ "orders": { "$not": { "$elemMatch": { "status": "PAID" } } } }`. ### Bitwise | Operator | Example | Rows returned | SQL | |---|---|---|---| | `$bitsAllSet` | `{ "flags": { "$bitsAllSet": [ 1, 5 ] } }` | with every one of the bits set | `(a & 34) = 34` | | `$bitsAnySet` | `{ "flags": { "$bitsAnySet": 34 } }` | with one of the bits set | `(a & 34) <> 0` | | `$bitsAllClear` | `{ "flags": { "$bitsAllClear": [ 1, 5 ] } }` | with none of the bits set | `(a & 34) = 0` | | `$bitsAnyClear` | `{ "flags": { "$bitsAnyClear": 34 } }` | with one of the bits not set | `(a & 34) <> 34` | The bits are a number, or the array of their positions (the first one is 0). Oracle is asked with `BITAND(a, 34)`. ### Geospatial For MongoDB instances, on a field with a `2dsphere` or `2d` index. | Operator | Example | |---|---| | `$geoWithin`, `$geoIntersects` | `{ "place": { "$geoWithin": { "$centerSphere": [ [ 72.5, 23.0 ], 0.01 ] } } }` | | `$near`, `$nearSphere` | `{ "place": { "$near": { "$geometry": { "type": "Point", "coordinates": [ 72.5, 23.0 ] }, "$maxDistance": 500 } } }` | ### Of API Maker | Operator | Example | Rows returned | |---|---|---| | `$like` | `{ "first_name": { "$like": "Bob%" } }` | SQL pattern, `%` any text, `_` one character, on every database | | `$isNull` | `{ "last_update": { "$isNull": true } }` | null, or not null with `false` (SQL databases) | `$like` compares the whole value with the pattern, as `LIKE` of SQL does, and returns the same rows on every database : | Pattern | Rows returned | |---|---| | `"Bob%"` | starting with `Bob` | | `"%son"` | ending with `son` | | `"%ar%"` | holding `ar` | | `"Bob"` | which are `Bob` | | `"B_b"`, `"B%b"` | `_` is any one character and `%` any text, wherever they stand | - **MongoDB** has no `LIKE` : API Maker asks it with the regular expression which matches the same values, `{ first_name: /^Bob/is }` for `"Bob%"`. Every other character of the pattern is itself, so `"%1+1%"` and `"%a.b%"` need no escaping, and the case is ignored. - **SQL instances** are asked with `LIKE` and the pattern itself. The collation of the column decides about the case, as for `=` : see [Regular expressions](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html#regular-expressions). ## null on a SQL instance A column of a table always exists : what it can lack is a value, and SQL calls that `NULL`. | find | SQL | |---|---| | `{ "last_update": null }`, `{ "last_update": { "$eq": null } }` | `"last_update" IS NULL` | | `{ "last_update": { "$ne": null } }` | `"last_update" IS NOT NULL` | | `{ "last_update": { "$exists": false } }`, `{ "last_update": { "$isNull": true } }` | `"last_update" IS NULL` | | `{ "pincode": { "$in": [ 382345, null ] } }` | `("pincode" IN (?) OR "pincode" IS NULL)` | | `{ "pincode": { "$nin": [ 382345, null ] } }` | `"pincode" NOT IN (?)` | > **Rows without a value and the operators which negate** > > For SQL a comparison with `NULL` is neither true nor false. A row whose column is `NULL` is returned by none of `$ne`, `$nin`, `$not` and `$nor` on a SQL instance, where MongoDB returns a document without the field. Ask for those rows too with `$or` : `{ "$or": [ { "pincode": { "$ne": 382345 } }, { "pincode": null } ] }`. ## Regular expressions `$regex` takes the regular expression as a string, with the options `i` (any case), `m` (`^` and `$` at every line), `s` (`.` matches a line break) and `x` (spaces in the expression are ignored) in `$options`. A query param takes it as `/^ma/i`. | Database | `{ "first_name": { "$regex": "^ma", "$options": "i" } }` | |---|---| | MongoDB | `{ first_name: /^ma/i }` | | MySQL, MariaDB | `` `first_name` REGEXP '(?i)^ma' `` | | PostgreSQL | `"first_name" ~ '(?i)^ma'` | | Oracle | `REGEXP_LIKE("first_name", '^ma', 'i')` | | SQL Server | `[first_name] LIKE 'ma%'` | - **Case** : without `i`, MongoDB, PostgreSQL and Oracle match the case as written. MySQL, MariaDB and SQL Server follow the collation of the column, as their `=` and `LIKE` do, and most collations ignore the case. - **SQL Server** has no regular expressions. An expression which says no more than `LIKE` can is written as `LIKE` : a text, `.`, `.*`, `[a-z]`, `\d`, with `^` and `$` at the ends. Any other expression is refused there with a message. - **`%` at an end** (`"%son"`, `"Ma%"`, `"%ar%"`) is read as the [`$like`](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html#of-api-maker) pattern, on every database. - `$not` takes a regular expression too : `{ "first_name": { "$not": { "$regex": "^ma" } } }`. ## $expr on a SQL instance `$expr` compares fields with each other. On a MongoDB instance it takes every expression of MongoDB. On a SQL instance it takes : - the comparisons `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, and `$and`, `$or`, `$not` around them ; - the arithmetic `$add`, `$subtract`, `$multiply`, `$divide`, `$mod`, `$abs` ; - a field as `"$price"`, a value as it is, and a text which starts with `$` as `{ "$literal": "$5" }`. **Orders which cost more than their budget, tax included** ```json { "$expr": { "$gt": [ { "$multiply": [ "$price", 1.18 ] }, "$budget" ] } } ``` **What a MySQL instance is asked** ```sql (`price` * ?) > `budget` ``` ## Find and join : fields of related tables A key with dots follows a column which refers to another table, as the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) declares it, and reaches a field there. API Maker reads the related table with its own query API, keeps the ids which match, and filters the main table with them : a join across tables, databases and even instances. > Diagram : find and join : the related table first, then the main table filtered by its ids > > A find on customer_id.loyalty_tier of the orders first reads the customers whose tier is GOLD, keeps their ids, then reads the orders whose customer_id is in those ids : two queries, which can be on two databases. **Orders of gold customers : orders.customer_id refers to customers** ```json { "find": { "customer_id.loyalty_tier": "GOLD" } } ``` **Two levels, with $and and $or : orders of customers who live in Pune, paid or shipped** ```json { "find": { "$and": [ { "customer_id.city_id.name": "Pune" }, { "$or": [ { "status": "PAID" }, { "status": "SHIPPED" } ] } ] } } ``` - The relation comes from the schema of the table only (`table` or `collection`, `column`, `database`, `instance` on the field), as many levels as the dots : a path the schema does not describe is refused. A `deep` item of the call does not make one. - Schema APIs only : the generated APIs (`/api/gen`) read no schema, and a dotted key goes to the database as it is (a field of an embedded document on MongoDB, an unknown column on SQL). - The related rows are read with the permissions of the caller : a field or a table the group can not read can not be filtered on. - A virtual field can not be used in a find and join : `Virtual field can not be used in find join feature. 'orders' is virtual field.` Filter the table which holds the reference instead : the orders of a customer, not the customers of an order. ## Types - **Schema APIs** convert the values to the types of the schema : `{ "customer_id": "8" }` matches the number 8, a date string becomes a date, an id string an ObjectId. That holds wherever a value stands : below an operator, in the array of `$in` or `$all`, inside `$not`, `$nor` and `$elemMatch`, and for the items of an array field (`{ "scores": { "$gt": "5" } }`). `null` stays `null`. - **Generated APIs** compare what you send : `{ "customer_id": "8" }` looks for the string. ## A column as a query param On get all, get by id and the streams, a column name is a filter without `find` : ```text ?customer_id=1 ?first_name=Bob,Alice # one of them ?pincode>=382345 # also > < <= != ?first_name=/^ma/i # regular expression ?first_name=Bob&find={pincode:382345} # both apply ``` ## From code **TypeScript** ```typescript const rows = await g.sys.db.query({ instance: 'mysql8', database: 'sales', collection: 'orders', find: { total: { $gte: 500 }, 'customer_id.loyalty_tier': 'GOLD' }, }); ``` ## Security `find` is powerful : with it a caller reads every row a table has, unless a [pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) narrows it to their own rows. The [APIs Security Report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) tells which tables need that hook and writes it for you. On SQL instances every value of a `find` is a parameter of the statement : a text which starts with `__:` is a value like any other, which matches the rows holding that text, never SQL. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.md) --- # select Parameter > Choose the fields of an answer with the select parameter of API Maker - a list of columns to keep, or a list to leave out with a minus sign, on reads and on the answers of writes, with $slice and $elemMatch for the arrays of MongoDB. Source: https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.html `select` keeps only the fields you name, or, with `-`, everything but the fields you name. Less data on the wire, and nothing a screen does not need. **Only these fields** ```text GET /api/schema/admin/mysql8/inventory/customers?select=first_name,last_name ``` **Answer : the primary key always comes along** ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 1, "first_name": "Bob", "last_name": "Lin" }, { "customer_id": 2, "first_name": "Alice", "last_name": "Page" } ] } ``` **Everything but these fields** ```text GET /api/schema/admin/mysql8/inventory/customers?select=-last_update,-isActive ``` **Body of the query APIs : a string or an object** ```json { "find": {}, "select": "first_name,last_name" } { "find": {}, "select": { "first_name": 1, "last_name": 1 } } ``` - On MongoDB a nested field selects too : `select=address.city`. - Do not mix included and excluded fields in one `select`. - The write APIs (save, master save, update by id, replace by id, remove by id) accept `select` to shape the row they answer with. - Inside a `deep` item, `select` picks the fields of the related rows : `deep=[{s_key:'customer_id',t_col:'orders',t_key:'customer_id',select:'order_no,total'}]`. - A field the group of the caller can not read never comes back, selected or not : that is the field permission of [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html). ## Arrays on MongoDB : $slice and $elemMatch A field of a MongoDB document can hold an array. `select` takes the two operators MongoDB has to shape one, on the generated and on the schema APIs : | Operator | Example | The array comes back with | |---|---|---| | `$slice` | `{ "tags": { "$slice": 2 } }` | its first 2 items, its last 2 with `-2` | | `$slice` | `{ "orders": { "$slice": [ 10, 5 ] } }` | 5 items after the first 10 : `[ skip, limit ]` | | `$elemMatch` | `{ "orders": { "$elemMatch": { "status": "PAID" } } }` | its first item which meets the conditions | **The last 3 orders of each customer** ```json { "find": {}, "select": { "first_name": 1, "orders": { "$slice": -3 } } } ``` **As a query param (JSON5)** ```text GET /api/schema/admin/mongodb/inventory/customers?select={first_name:1,orders:{$slice:-3}} ``` - Next to fields with `1`, those fields and the array come back. Alone, `$slice` leaves every other field in, and `$elemMatch` returns the array with the id only, as MongoDB does. - The conditions of `$elemMatch` are the ones of [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html). The schema APIs convert their values to the types of the schema : `{ "order_id": "5" }` matches the number 5, an id string an ObjectId. - `$elemMatch` is for an array at the top of the document. `$slice` cuts a nested one too : `{ "orders.items": { "$slice": 1 } }`. - On the schema APIs `$slice` takes numbers only, and any other object in `select` selects the whole field : nothing is computed from other fields. - `select` of a `deep` item and of the write APIs takes them too. - A table of a SQL instance holds no arrays : there the field is selected as it is. - [deep →](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html) - [sort](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#sort) --- # deep Parameter (Deep Populate) > Replace ids with the rows they point to with the deep parameter of API Maker - from any table, database or instance, several levels deep, with a filter, a sort, a limit and a selection per level, one query per deep item. Source: https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html `deep` replaces an id with the row it points to. The related table can be in the same database, in another one, or on another instance of another kind : a MongoDB collection populated from a PostgreSQL table works. API Maker reads each related table once for all the parent rows : one query per deep item, whatever their number (for a virtual field, one per 1000 parents). **Query param (JSON5)** ```text GET /api/schema/admin/postgresql/inventory/cities?deep=[{s_key:'state_id',t_instance:'oracle',t_db:'inventory',t_col:'states',t_key:'id'}] ``` **Body of the query APIs** ```json { "find": {}, "deep": [ { "s_key": "state_id", "t_instance": "oracle", "t_db": "inventory", "t_col": "states", "t_key": "id" } ] } ``` > Diagram : deep : one query per level, for all the parents at once > > deep on a get all of cities reads the cities, then the states of all those cities in one query with $in, then the countries of all those states in one more query, even when the tables are on other instances, and nests each row in its parent. ## The tables of the examples | cities (PostgreSQL) | | | states (Oracle) | | | countries (MySQL) | | |---|---|---|---|---|---|---|---| | id | state_id | city_name | id | country_id | state_name | id | country_name | | 101 | 201 | AHMEDABAD | 201 | 301 | GUJARAT | 301 | INDIA | | 105 | 202 | KOCHI | 202 | 301 | KERALA | 302 | USA | | 110 | 204 | DUNCAN | 204 | 302 | ARIZONA | | | ## One level ```json { "find": { "id": 101 }, "deep": [ { "s_key": "state_id", "t_instance": "oracle", "t_db": "inventory", "t_col": "states", "t_key": "id" } ] } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "id": 101, "city_name": "AHMEDABAD", "state_id": { "id": 201, "country_id": 301, "state_name": "GUJARAT" } } ] } ``` ## The keys of a deep item | Key | Meaning | |---|---| | `s_key` | The field of the current table which holds the id (the **source key**). Required. | | `t_col` | The related table or collection (the **target**). Not needed when the schema gives it. | | `t_key` | The field of the related table to match. Its primary key when absent. | | `t_db`, `t_instance` | The database and the instance of the related table. The ones of the current table when absent. | | `isMultiple` | `true` : several related rows are expected, the field becomes an array. `false` (default) : one row, the field becomes an object. | | `find` | A filter on the related rows : `{ "status": "PAID" }`. | | `select` | The fields of the related rows to keep. | | `sort`, `skip`, `limit` | Order and paging of the related rows, per parent row. | | `deep` | The next level : an array of deep items on the related table. | | `fetchingTechnique` | For virtual fields : `chunk` (default, 1000 ids per query) or `one_by_one` (one query per parent, needed with `skip` and `limit` on big sets). `fetchingTechniqueSettings.chunkSize` changes the chunk. | ## Several levels ```json { "find": {}, "limit": 1, "deep": [ { "s_key": "state_id", "t_instance": "oracle", "t_db": "inventory", "t_col": "states", "t_key": "id", "deep": [ { "s_key": "country_id", "t_instance": "mysql", "t_db": "inventory", "t_col": "countries", "t_key": "id" } ] } ] } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "id": 101, "city_name": "AHMEDABAD", "state_id": { "id": 201, "state_name": "GUJARAT", "country_id": { "id": 301, "country_name": "INDIA" } } } ] } ``` - As many levels as you need. Each deep item is one query on its table, for all the parents at once. ## One to many **Every order of each customer, the latest 5, paid ones only** ```json { "find": {}, "deep": [ { "s_key": "id", "t_col": "orders", "t_key": "customer_id", "isMultiple": true, "find": { "status": "PAID" }, "sort": "-created_at", "limit": 5, "select": "order_no,total" } ] } ``` - `isMultiple: true` matches every related row and gives an array. - The `id` of each customer is replaced by its orders. To keep it and add the orders beside it, declare a virtual field (below). ## Two relations at once ```json { "deep": [ { "s_key": "state_id", "t_col": "states", "t_key": "id" }, { "s_key": "created_by", "t_col": "users", "t_key": "id", "select": "name" } ] } ``` ## With the schema : just the source key When the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table names the relation on the field (`collection` or `table`, `column`, and `database`, `instance` when they differ), the deep item only needs `s_key`, and the target is never wrong twice : **cities.schema.ts** ```typescript state_id: { __type: EType.number, instance: 'oracle', database: 'inventory', collection: 'states', column: 'id' }, ``` ```text GET /api/schema/admin/postgresql/inventory/cities?deep=[{s_key:'state_id'}] GET /api/schema/admin/postgresql/inventory/cities?deep=[{s_key:'state_id',deep:[{s_key:'country_id'}]}] ``` - A **virtual field** (`isVirtualField` with `s_columnVirtualLinker` and `t_columnVirtualLinker`) is a one to many relation stored on the other side : `deep=[{s_key:'orders',isMultiple:true}]` on a customer fills its orders from the `orders` table, without a column on the customer. Without `isMultiple` it holds one row. A `select` of the item must keep the column `t_columnVirtualLinker` names, or no row can be matched. - The generated APIs (`/api/gen`) do not read the schema : give `t_col` and `t_key` in every deep item. There, `deep` works from a MongoDB collection ; from a SQL table, call the schema APIs (`/api/schema`), the generated ones answer `Unable to find collection`. ## On writes `deep` on save single or multiple, master save, update by id, replace by id and remove by id populates the row of the answer : ```text POST /api/schema/admin/mysql8/inventory/cities/save-single-or-multiple?deep=[{s_key:'state_id'}] ``` ## Flat answers The header `x-am-response-object-type: make_flat` turns the nested objects into one level with `_` between the names : `state_id_country_id_country_name`. See [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-response-object-type). ## Good to know - The related rows are read through the query API of their table : the columns the group of the caller may not read are removed, and a table it may not call makes the item fail (`errors`). - For a request of a person or an application, they are read as that caller reads them : the pre and post hooks of the query API of their table run, as for a call of that API. A hook which keeps a person to their own rows keeps them there in a `deep` too, and a table whose settings say `NO_ACCESS` is not read. API Maker version 3 read them without these hooks. - A `deep` asked by code (`g.sys.db`, in a custom API, a hook, a scheduler ...) follows the code : the hooks of the related table run only when the call passes `skipHookRunning: false`, as for the table it calls. - A `deep` item which finds no table, or names a column the table does not have, answers 400 : `errors` names it, and `data` still holds the rows. On writes, those errors are dropped. - Filtering the parent rows by a field of a related table is a different feature, [find and join](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html#find-and-join-fields-of-related-tables), which combines with `deep`. --- # Get All API > Read the rows of a table with the get all API of API Maker - filter with find or any column, page with skip and limit, sort, select fields, populate related rows with deep, and get the total count. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.html Reads rows of a table. Without a query param it returns every row ; with `find`, `skip`, `limit`, `sort`, `select`, `deep` and `getTotalCount` it becomes the list API of your screens. | | | |---|---| | Method | GET | | URL | `/api/schema/admin/mysql8/inventory/customers` | | Body | none | | Query params | [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html), [skip](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#skip), [limit](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#limit), [sort](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#sort), [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.html), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html), [getTotalCount](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#getTotalCount), any column | | Answer | `data` : an array of rows, `totalCount` with `getTotalCount=true` | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, with `enableCaching` on the table | | From code | [`g.sys.db.getAll`](https://docs.apimaker.dev/v4/examples/sys/db/getAll.html) | | API id | `SCHEMA_GET_ALL` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## Every row ```text GET /api/schema/admin/mysql8/inventory/customers ``` ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 1, "first_name": "Bob", "last_name": "lin", "last_update": "2022-11-14T04:34:58.000Z", "pincode": 382345, "isActive": 1 }, { "customer_id": 2, "first_name": "Alice", "last_name": "Page", "last_update": "2022-10-15T02:10:40.000Z", "pincode": 382346, "isActive": 1 } ] } ``` - Without `limit`, every row comes back. Add `limit` as soon as the table can grow. ## Filter with a column name Any column is a query param. The operators of [api-query-params](https://github.com/loris/api-query-params) work in the key : `>`, `>=`, `<`, `<=`, `!=`, a comma for a list, a regex between slashes. ```text GET /api/schema/admin/mysql8/inventory/customers?customer_id=1 GET /api/schema/admin/mysql8/inventory/customers?first_name=Bob&last_name=lin # both must match GET /api/schema/admin/mysql8/inventory/customers?first_name=Bob,Alice # one of them GET /api/schema/admin/mysql8/inventory/customers?pincode>=382346 GET /api/schema/admin/mysql8/inventory/customers?first_name!=Bob GET /api/schema/admin/mysql8/inventory/customers?first_name=/^ma/i # regular expression ``` - Numbers, booleans and ISO dates in a query param are cast to their type before the query. ## Filter with find `find` takes a JSON5 object with the query operators of MongoDB, on every database : `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$and`, `$or`, `$nor`, `$not`, `$exists`, `$regex`, `$mod`, `$expr` and the others, with `$like` and `$isNull` of API Maker. See [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html) for every operator and what it becomes in SQL. ```text GET /api/schema/admin/mysql8/inventory/customers?find={first_name:'Bob'} GET /api/schema/admin/mysql8/inventory/customers?find={customer_id:{$in:[1,2,3]}} GET /api/schema/admin/mysql8/inventory/customers?find={$or:[{pincode:382345},{first_name:'Alice'}]} GET /api/schema/admin/mysql8/inventory/customers?find={first_name:{$like:'Ma%'}} ``` ## Page and sort ```text GET /api/schema/admin/mysql8/inventory/customers?skip=20&limit=10&sort=-last_update,first_name ``` - `sort` takes column names separated by commas, `-` for descending. The JSON form `sort={last_update:-1}` works too. - `skip` and `limit` are numbers. `limit` caps the rows of the answer ; `skip` leaves out the first rows. ## Select fields ```text GET /api/schema/admin/mysql8/inventory/customers?select=first_name,last_name # only these (the primary key stays) GET /api/schema/admin/mysql8/inventory/customers?select=-last_update,-isActive # everything but these ``` ## Related rows with deep `deep` replaces an id with the row it points to, from any table of any instance, as many levels as you want. See [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html). ```text GET /api/schema/admin/mysql8/inventory/customers?deep=[{s_key:'customer_id',t_col:'orders',t_key:'customer_id',isMultiple:true,select:'order_no,total'}] ``` ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 1, "first_name": "Bob", "last_name": "lin", "orders": [ { "order_no": 1001, "total": 12900 } ] } ] } ``` ## Related rows from the schema When the schema of the table names the target of a field (`collection`, `column`, `database`, `instance`), the `deep` item only needs the source key : ```text GET /api/schema/admin/mysql8/inventory/customers?deep=[{s_key:'shipping_id'}] ``` ## Total count ```text GET /api/schema/admin/mysql8/inventory/customers?find={isActive:1}&skip=0&limit=10&getTotalCount=true ``` ```json { "success": true, "statusCode": 200, "data": [ "…10 rows…" ], "totalCount": 5 } ``` - `totalCount` counts every row matching the filter, before `skip` and `limit` : what a pager needs. ## Everything together ```text GET /api/schema/admin/mysql8/inventory/customers?find={first_name:'Bob'}&skip=1&limit=4&sort=-customer_id&select=first_name&deep=[{s_key:'customer_id',t_col:'orders',t_key:'customer_id'}]&getTotalCount=true ``` ## Caching, hooks, notifications - With [caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) on for the table, the answer comes from Redis until a write through API Maker changes the table ; the response header `x-am-data-source` says `cache` or `api`, and `x-am-cache-control: reset_cache` forces the database. - [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) can add conditions to `g.req.query.find` (for example the rows of the signed-in person) and post hooks can reshape `g.res.output`. When the answer comes from the cache, the hooks do not run. - Clients subscribed to this table with [WebSocket events](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html) are notified after a successful call. ## From your code [`g.sys.db.getAll`](https://docs.apimaker.dev/v4/examples/sys/db/getAll.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers` | | API id | `GEN_GET_ALL` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.getAllGen`](https://docs.apimaker.dev/v4/examples/sys/db/getAll.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Get All by Stream API > Stream the rows of a table with the get all by stream API of API Maker - the same filters as get all, sent as they are read, for exports and large result sets without holding everything in memory. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.html The same query as [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.html), answered as a stream : rows go to the client as the database produces them, so a million rows cost the server a few kilobytes of memory, not the whole result. | | | |---|---| | Method | GET | | URL | `/api/schema/admin/mysql8/inventory/customers/stream` | | Body | none | | Query params | the same as get all : find, skip, limit, sort, select, deep, getTotalCount, any column | | Answer | `data` : an array of rows, streamed | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no : streams always come from the database | | From code | [`g.sys.db.getAllByStream`](https://docs.apimaker.dev/v4/examples/sys/db/getAllByStream.html) | | API id | `SCHEMA_GET_ALL_STREAM` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/stream` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/stream` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The call ```text GET /api/schema/admin/mysql8/inventory/customers?find={isActive:1}&sort=customer_id&select=customer_id,first_name ``` ```json { "data": [ { "customer_id": 1, "first_name": "Bob" }, { "customer_id": 2, "first_name": "Alice" } ], "success": true, "statusCode": 200, "meta": {} } ``` - The envelope is the same as [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.html), but `data` comes first and its rows are written in chunks while the query runs. `success`, `statusCode`, `meta` and `totalCount` close the answer. Parse it as JSON when it is complete, or read it with a streaming JSON parser as it arrives. - If the database fails while rows were already sent, the answer still ends as valid JSON : `], "success": false, "statusCode": 500, "errors": [...] }`. Read `success` at the end. ## When to use it - Exports, reports, synchronisations, copies of a table : anything which reads many rows and would not fit a normal answer. - Every filter of get all works : `find`, `sort`, `select`, `deep`, `skip`, `limit`. - The answer is never cached, and the rows are never compressed in memory : `x-am-data-source` is always `api`. ## Hooks on a stream - Pre hooks run before the query and can change it. - Post hooks run when the stream ends. They can not change what was sent, and `g.res.output` holds only the last chunk of rows, so do not use a post hook to reshape a stream : reshape in a custom API around `g.sys.db.getAllByStream`. ## From your code In a custom API the same call hands you every row as it arrives, without building the whole array : **TypeScript** ```typescript let count = 0; await g.sys.db.getAllByStream({ instance: 'mysql8', database: 'inventory', collection: 'customers', queryParams: { find: { isActive: 1 } }, }, (row) => { count++; // one object at a time, in order }); return { count }; ``` ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/stream` | | API id | `GEN_GET_ALL_STREAM` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.getAllByStreamGen`](https://docs.apimaker.dev/v4/examples/sys/db/getAllByStream.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.md) --- # Get by ID API > Read one row by its primary key with the get by id API of API Maker, or by any other column with the primaryKey param, with select and deep populate. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-by-id-api.html Reads one row by its primary key, or by any other column with the second param. With `select` and `deep`, it is the detail screen of your app in one call. | | | |---|---| | Method | GET | | URL | `/api/schema/admin/mysql8/inventory/customers/get-by-id/:id[/:primaryKey]` | | Body | none | | Query params | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.html), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html), find (extra conditions) | | Answer | `data` : the row, or `null` when nothing matches | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, with `enableCaching` on the table | | From code | [`g.sys.db.getById`](https://docs.apimaker.dev/v4/examples/sys/db/getById.html) | | API id | `SCHEMA_GET_BY_ID` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/get-by-id/:id[/:primaryKey]` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-by-id-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/get-by-id/:id[/:primaryKey]` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## By primary key ```text GET /api/schema/admin/mysql8/inventory/customers/get-by-id/1 ``` ```json { "success": true, "statusCode": 200, "data": { "customer_id": 1, "first_name": "Bob", "last_name": "lin", "last_update": "2022-11-14T04:34:58.000Z", "pincode": 382345, "isActive": 1 } } ``` - The primary key is the one of the table (`_id` on MongoDB, the primary key column on SQL, or `isPrimaryKey` in the schema). An unknown id answers `data: null` with `success: true`. ## By any column Name the column as the second param : ```text GET /api/schema/admin/mysql8/inventory/customers/get-by-id/Bob/first_name GET /api/schema/admin/mysql8/inventory/customers/get-by-id/382345/pincode ``` - When several rows match, the first one is returned. - The value is converted to the type of the column, so `382345` is compared as a number. ## Fields and related rows ```text GET /api/schema/admin/mysql8/inventory/customers/get-by-id/1?select=first_name,last_name GET /api/schema/admin/mysql8/inventory/customers/get-by-id/1?deep=[{s_key:'customer_id',t_col:'orders',t_key:'customer_id',isMultiple:true}] ``` - `deep` works as on [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.html#related-rows-with-deep), with the relations of the schema when the table has one. ## One more condition `find` narrows the match further, for example to make sure the row belongs to the caller : ```text GET /api/schema/admin/mysql8/inventory/customers/get-by-id/1?find={isActive:1} ``` - A pre hook can add such a condition to `g.req.query.find` for every call : the row scoping pattern of [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html). ## From your code [`g.sys.db.getById`](https://docs.apimaker.dev/v4/examples/sys/db/getById.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/get-by-id/:id[/:primaryKey]` | | API id | `GEN_GET_BY_ID` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.getByIdGen`](https://docs.apimaker.dev/v4/examples/sys/db/getById.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Save Single or Multiple API > Insert one row or many with the save single or multiple API of API Maker - one object or an array in the body, generated ids, and select or deep on the answer. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-save-single-or-multiple-api.html Inserts one row (an object) or many (an array) and answers `201` with what was saved, ids included. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/save-single-or-multiple` | | Body | one object, or an array of objects | | Query params | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.html), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html) : applied to the answer | | Answer | `201` and `data` : the saved row, or the array of saved rows | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no (a write ; it resets the cache of the table) | | From code | [`g.sys.db.saveSingleOrMultiple`](https://docs.apimaker.dev/v4/examples/sys/db/saveSingleOrMultiple.html) | | API id | `SCHEMA_POST_BULK_INSERT` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/save-single-or-multiple` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-save-single-or-multiple-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/save-single-or-multiple` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## One row ```text POST /api/schema/admin/mysql8/inventory/customers/save-single-or-multiple ``` **Body** ```json { "first_name": "Bob", "last_name": "Lin", "pincode": 382345 } ``` **Answer** ```json { "success": true, "statusCode": 201, "data": { "customer_id": 6, "first_name": "Bob", "last_name": "Lin", "pincode": 382345 } } ``` - The primary key can be sent (`"customer_id": 27`) or left out : the database or API Maker generates it (`isAutoIncrementByDB`, `isAutoIncrementByAM`, `isAutoGenerateByAM` in the schema, `_id` on MongoDB). ## Many rows **Body** ```json [ { "first_name": "Bob", "last_name": "Lin" }, { "customer_id": 29, "first_name": "Eve", "last_name": "Page" } ] ``` - The answer is an array in the same order. Objects with and without a primary key can mix. - Every row is checked before anything is written ; an error names the object with `dataIndex`. ## What the schema does to each row - Unknown keys are refused, values converted to their types, strings trimmed and cased, `conversionFun` run, fields encrypted or hashed, ids and defaults filled, then the rules and `validatorFun` run. Every error is returned at once with `400` and nothing is saved. See [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html). - A field with a relation (`collection` and `column` in the schema) accepts a nested object : it is saved to its table first and replaced by its id. A failure reverts the rows saved so far. For nested updates and arrays, see [master save](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-master-save-api.html). **Body : the state is saved to the states table, then the city** ```json { "city_name": "Wembley", "state_id": { "state_name": "London", "country_id": 302 } } ``` ## The answer you want ```text POST /api/schema/admin/mysql8/inventory/customers/save-single-or-multiple?select=customer_id,first_name POST /api/schema/admin/mysql8/inventory/customers/save-single-or-multiple?deep=[{s_key:'shipping_id',t_col:'shippings',t_key:'id'}] ``` - `select` keeps only some fields of the saved rows in the answer ; `deep` populates their relations. ## After the save - The cache of the table is reset, subscribed WebSocket clients are notified, events with an automatic trigger on this API run, and post hooks get the saved rows in `g.res.output`. ## From your code [`g.sys.db.saveSingleOrMultiple`](https://docs.apimaker.dev/v4/examples/sys/db/saveSingleOrMultiple.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/save-single-or-multiple` | | API id | `GEN_POST_BULK_INSERT` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.saveSingleOrMultipleGen`](https://docs.apimaker.dev/v4/examples/sys/db/saveSingleOrMultiple.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. - On MongoDB a nested object is stored as an embedded document, as sent. It is not saved to another collection : that needs the relations of a schema and the schema API above. - On a SQL table every key must be a column. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Master Save API > Save or update a tree of related objects in one call with the master save API of API Maker - each object is inserted or updated by its primary key, nested objects go to their own tables, arrays fill one-to-many relations, and a failure reverts everything. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-master-save-api.html One call which saves or updates a whole object graph : each object is inserted when it has no primary key and updated when it has one, at every level of nesting, across tables and even databases. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/master-save` | | Body | one object, or an array of objects, with nested related objects | | Query params | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.html), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html) : applied to the answer | | Answer | `201` and `data` : the saved object(s), with the ids of the nested rows | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no (a write ; it resets the cache of the table) | | From code | [`g.sys.db.masterSave`](https://docs.apimaker.dev/v4/examples/sys/db/masterSave.html) | | API id | `SCHEMA_MASTER_SAVE` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/master-save` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-master-save-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/master-save` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## Save or update ```text POST /api/schema/admin/mysql8/inventory/customers/master-save ``` **Body : no primary key, inserted** ```json { "first_name": "Bob", "last_name": "Lin" } ``` **Body : a primary key which exists, updated with these fields** ```json { "customer_id": 27, "pincode": 382330 } ``` - The rule is the same at every level : **primary key present, the row is read and updated ; absent, the row is inserted.** An update sends only the fields you give. - An array in the body saves or updates each object and answers an array. ## A tree of objects With relations in the schemas (`collection`, `column`, `database`, `instance` on a field), a nested object is saved or updated in its own table first and its id is written in the parent : **Body : three tables, one call** ```json { "city_name": "Wembley", "state_id": { "state_name": "London", "country_id": { "country_name": "UK" } } } ``` **Answer** ```json { "success": true, "statusCode": 201, "data": { "id": 107, "city_name": "Wembley", "state_id": 205 } } ``` - The country is saved first, its id goes into the state, the state is saved, its id goes into the city. The tables can be in different databases and instances : the relation says where. > Diagram : Master save : the deepest object first, its id goes up into its parent > > Master save of a city with its state and its country : the deepest object is saved first. The country is inserted and its id goes into the state, the state is inserted and its id goes into the city, then the city is saved. A failure on the way reverts every row the call wrote. - `deep` on the call gives the nested rows back populated : `?deep=[{s_key:'state_id',deep:[{s_key:'country_id'}]}]`. ## One to many with virtual fields A [virtual field](https://docs.apimaker.dev/v4/docs/schema/schema.html) (`isVirtualField` with `s_columnVirtualLinker` and `t_columnVirtualLinker`) takes an array : each item is saved or updated in the target table with the key of the parent. **Body : an order and its items** ```json { "order_no": 1001, "customer_id": 1, "items": [ { "product_id": "66fa1c9a9b1e4a0012a3c002", "qty": 2 }, { "id": 55, "qty": 3 } ] } ``` - The first item is inserted, the second (it has its primary key) is updated. ## When something fails - Every row written by the call is reverted when a later one fails : a wrong field in the country of the example above leaves no city and no state behind. - The errors name the object and the field, with `dataIndex` for arrays. ## Optimistic concurrency control - A table whose schema has a version field (`isConcurrencyControlField`) requires the version in every update of master save, and refuses a stale one with `400`. See [Optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.html). ## From your code [`g.sys.db.masterSave`](https://docs.apimaker.dev/v4/examples/sys/db/masterSave.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/master-save` | | API id | `GEN_MASTER_SAVE` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.masterSaveGen`](https://docs.apimaker.dev/v4/examples/sys/db/masterSave.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. - Without relations, a nested object is stored as an embedded document on MongoDB and refused on SQL. The tree of tables needs the schema API above. - Save or update by primary key works : `_id` on MongoDB, the primary key column on SQL. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Array Operations API > Push, add to set, pull, pull all, pop and set elements of array fields in MongoDB documents with the array operations API of API Maker, several operations in one call. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-array-operations-api.html Changes array fields of MongoDB documents in place : push, addToSet, pull, pullAll, pop and set, on every document matching `find`, several operations in one call. | | | |---|---| | Method | PUT | | URL | `/api/schema/admin/mysql8/inventory/customers/array-operations` | | Body | `{ find, select?, operations: [ … ] }` | | Query params | none | | Answer | `data` : one array per operation with the documents matched, projected to the array (or `select`) | | Databases | MongoDB only | | Cached | no (a write ; it resets the cache of the table) | | From code | [`g.sys.db.arrayOperations`](https://docs.apimaker.dev/v4/examples/sys/db/arrayOperations.html) | | API id | `SCHEMA_ARRAY_OPERATIONS` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/array-operations` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-array-operations-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/array-operations` : replace `admin` with the user path of your account, `mongodb` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The body ```json { "find": { "customer_id": 1 }, "operations": [ { "operation": "push", "path": "ages", "dataToPush": [ { "age": 33, "birth_year": 1989 } ] } ] } ``` | Key | Meaning | |---|---| | `find` | Which documents to change. `{}` : all of them. | | `select` | The fields to return for the matched documents. Without it, the array of `path`. | | `operations` | Run one after the other, each on every matched document. | | Operation | Keys | Does | |---|---|---| | `push` | `path`, `dataToPush` (array), `position`, `slice`, `sort` | Appends the items. `position` inserts at an index, `slice` keeps the first N (or last N when negative), `sort` (`{ field: 1 or -1 }`) reorders. | | `addToSet` | `path`, `dataToPush` (one value or an array) | Appends only the values which are not there yet. | | `pull` | `path`, `queryToRemove` | Removes every item matching the query. | | `pullAll` | `path`, `dataToPull` (array) | Removes every item equal to one of the values. | | `pop` | `path`, `direction` | `1` removes the last item, `-1` the first. | | `set` | `dataToSet`, `arrayFilters`, `upsert` | Updates fields of items with the positional `$[item]` syntax and its filters. | ## Push and pull **Push two items at the front, keep the array at 10** ```json { "find": {}, "operations": [ { "operation": "push", "path": "ages", "dataToPush": [ { "age": 2 }, { "age": 5 } ], "position": 0, "slice": 10, "sort": { "age": 1 } } ] } ``` **Remove the items of a year** ```json { "find": { "customer_id": 1 }, "operations": [ { "operation": "pull", "path": "ages", "queryToRemove": { "birth_year": 1989 } } ] } ``` **Remove exact values** ```json { "find": {}, "operations": [ { "operation": "pullAll", "path": "tags", "dataToPull": [ "old", "draft" ] } ] } ``` ## Set inside an array ```json { "find": {}, "operations": [ { "operation": "set", "dataToSet": { "ages.$[item].age": 45, "ages.$[item].birth_year": 1999 }, "arrayFilters": [ { "item.birth_year": 1999 } ], "upsert": true } ] } ``` - `arrayFilters` decide which items `$[item]` means, `upsert: true` inserts a document when `find` matches none. ## Several operations at once ```json { "find": {}, "operations": [ { "operation": "push", "path": "ages", "dataToPush": [ { "age": 60 } ] }, { "operation": "pull", "path": "ages", "queryToRemove": { "age": 2 } } ] } ``` **Answer : one entry per operation** ```json { "success": true, "statusCode": 200, "data": [ [ { "_id": "63e0775abd0e063920533f7c", "ages": [ { "age": 33 }, { "age": 60 } ] } ], [ { "_id": "63e0775abd0e063920533f7c", "ages": [ { "age": 33 }, { "age": 60 } ] } ] ] } ``` ## With a schema - The items pushed, added or set are converted and validated against the schema of the array field before they are written, like a save. Encrypted fields inside an array are handled too. ## From your code [`g.sys.db.arrayOperations`](https://docs.apimaker.dev/v4/examples/sys/db/arrayOperations.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/array-operations` | | API id | `GEN_ARRAY_OPERATIONS` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.arrayOperationsGen`](https://docs.apimaker.dev/v4/examples/sys/db/arrayOperations.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Update by ID API > Change some fields of one row with the update by id API of API Maker - by primary key or any column, with upsert, returnDocument, select and deep, and the version check of optimistic concurrency control. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.html Changes the fields you send on one row and leaves the others alone. The row is found by its primary key, or by the column you name. | | | |---|---| | Method | PUT | | URL | `/api/schema/admin/mysql8/inventory/customers/update-by-id/:id[/:primaryKey]` | | Body | the fields to change | | Query params | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.html), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html), [upsert](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#upsert), [returnDocument](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#returnDocument), throwErrorIfRecordNotFound, find | | Answer | `data` : the row after the change (or before, with `returnDocument=before`) | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no (a write ; it resets the cache of the table) | | From code | [`g.sys.db.updateById`](https://docs.apimaker.dev/v4/examples/sys/db/updateById.html) | | API id | `SCHEMA_PUT_UPDATE_BY_ID` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/update-by-id/:id[/:primaryKey]` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/update-by-id/:id[/:primaryKey]` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## Some fields ```text PUT /api/schema/admin/mysql8/inventory/customers/update-by-id/1 ``` **Body** ```json { "pincode": 382330 } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "customer_id": 1, "first_name": "Bob", "last_name": "lin", "last_update": "2022-11-14T04:34:58.000Z", "pincode": 382330, "isActive": 1 } } ``` - Send only what changes. Several fields at once are fine. On MongoDB, a nested key like `"address.city"` updates that path. ## By another column ```text PUT /api/schema/admin/mysql8/inventory/customers/update-by-id/Bob/first_name ``` - The row whose `first_name` is `Bob` is updated. The column named as the key can itself be in the body : `/update-by-id/Mallory/first_name` with `{ "first_name": "Alice" }` renames her. ## Insert when missing : upsert ```text PUT /api/schema/admin/mysql8/inventory/customers/update-by-id/999?upsert=true ``` **Body** ```json { "first_name": "Daphney", "last_name": "Sonia", "pincode": 980220 } ``` - No row with `customer_id` 999 : one is inserted with the id and the body (the body is validated as a save). A row exists : it is updated. On every database. ## The row before the change ```text PUT /api/schema/admin/mysql8/inventory/customers/update-by-id/1?returnDocument=before ``` - The answer holds the row as it was ; the update happens all the same. Default : `after`. See [returnDocument](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#returnDocument). ## Unknown id - By default an unknown id answers `success: true` with `data: null` and changes nothing. With `?throwErrorIfRecordNotFound=true` it answers `404` and `Record not found.` - `find` adds conditions the row must match : `?find={isActive:1}`. A pre hook can add the condition for every call. ## Fields and related rows in the answer ```text PUT /api/schema/admin/mysql8/inventory/customers/update-by-id/1?select=first_name,last_name&deep=[{s_key:'customer_id',t_col:'orders',t_key:'customer_id'}] ``` ## What the schema does - The fields sent are converted and validated like a save (types, trim, case, `conversionFun`, encryption, rules, `validatorFun`) ; unknown keys are refused ; `required` only applies to the fields present. Every error comes back at once with `400`. - A nested object on a relation field is saved or updated in its table and replaced by its id, as in [master save](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-master-save-api.html). - **Version check.** When the schema has a field with `isConcurrencyControlField`, the body must carry it and its value must be the one of the row : otherwise `400` with `Concurrency version mismatch in 'version'. This row/document is already updated.` and nothing changes. See [Optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.html). ## From your code [`g.sys.db.updateById`](https://docs.apimaker.dev/v4/examples/sys/db/updateById.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/update-by-id/:id[/:primaryKey]` | | API id | `GEN_PUT_UPDATE_BY_ID` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.updateByIdGen`](https://docs.apimaker.dev/v4/examples/sys/db/updateById.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Update Many API > Change every row matching a filter in one statement with the update many API of API Maker - a find and an updateData in the body, the count of changed rows in the answer. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-many-api.html Applies the same change to every row matching `find`, in one statement of the database, and answers how many rows changed. | | | |---|---| | Method | PUT | | URL | `/api/schema/admin/mysql8/inventory/customers/update-many` | | Body | `{ find, updateData }` | | Query params | none | | Answer | `data` : `{ updatedRowsCount }` | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no (a write ; it resets the cache of the table) | | From code | [`g.sys.db.updateMany`](https://docs.apimaker.dev/v4/examples/sys/db/updateMany.html) | | API id | `SCHEMA_UPDATE_MANY` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/update-many` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-many-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/update-many` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The call ```text PUT /api/schema/admin/mysql8/inventory/customers/update-many ``` **Body** ```json { "find": { "customer_id": { "$in": [ 2, 3 ] } }, "updateData": { "last_name": "Brown" } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "updatedRowsCount": 2 } } ``` - `find` takes every operator of [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html) : `$in`, `$gt`, `$and`, `$or`, `$like`, dotted keys… `{}` updates every row. - `updatedRowsCount` counts the rows the database actually changed. ## More filters **$and** ```json { "find": { "$and": [ { "isActive": 1 }, { "pincode": { "$gt": 382345 } } ] }, "updateData": { "isActive": 0 } } ``` **$or** ```json { "find": { "$or": [ { "first_name": "Bob" }, { "first_name": "Alice" } ] }, "updateData": { "pincode": 380001 } } ``` **a related table (find and join) : the orders of one customer, orders.customer_id refers to customers** ```json { "find": { "customer_id.email": "gone@example.com" }, "updateData": { "status": "CANCELLED" } } ``` ## What the schema does - `updateData` is converted and validated like an update (types, `conversionFun`, encryption, rules) and `find` like a query, before the statement runs. - The version check of optimistic concurrency control does not apply here : the rows are changed directly in the database, without being read first. ## Good to know - Nothing is read back : the answer has no rows. Read them with [query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-api.html) if you need them. - The cache of the table is reset and WebSocket subscribers are notified once for the call. ## From your code [`g.sys.db.updateMany`](https://docs.apimaker.dev/v4/examples/sys/db/updateMany.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/update-many` | | API id | `GEN_UPDATE_MANY` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.updateManyGen`](https://docs.apimaker.dev/v4/examples/sys/db/updateMany.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Replace by ID API > Replace a whole MongoDB document with the replace by id API of API Maker - by primary key or any column, with upsert, returnDocument, select and deep. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-replace-by-id-api.html Replaces one MongoDB document with the body : fields you do not send are gone. For a partial change, use [update by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.html). | | | |---|---| | Method | PUT | | URL | `/api/schema/admin/mongodb/inventory/customers/replace-by-id/:id[/:primaryKey]` | | Body | the whole new document | | Query params | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.html), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html), [upsert](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#upsert), [returnDocument](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html#returnDocument), throwErrorIfRecordNotFound | | Answer | `data` : the document after the change (or before, with `returnDocument=before`) | | Databases | MongoDB only | | Cached | no (a write ; it resets the cache of the table) | | From code | [`g.sys.db.replaceById`](https://docs.apimaker.dev/v4/examples/sys/db/replaceById.html) | | API id | `SCHEMA_PUT_REPLACE_BY_ID` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mongodb/inventory/customers/replace-by-id/:id[/:primaryKey]` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-replace-by-id-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mongodb/inventory/customers/replace-by-id/:id[/:primaryKey]` : replace `admin` with the user path of your account, `mongodb` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The call ```text PUT /api/schema/admin/mongodb/inventory/customers/replace-by-id/60ae24c8c37cd955cc144162 ``` **Body** ```json { "customer_id": 1, "first_name": "Rex", "last_name": "Nestor", "pincode": 879654, "isActive": 0 } ``` - The document keeps its `_id` and gets exactly these fields. A field of the old document which is not in the body disappears. - By another column : `/replace-by-id/4/customer_id`. ## Options - `?upsert=true` inserts the document when the id does not exist. - `?returnDocument=before` answers the old document. - `?throwErrorIfRecordNotFound=true` answers `404` instead of `data: null` for an unknown id. - `select` and `deep` shape the answer. ## What the schema does - The body is treated as a full save : conversions, defaults, generated values and every rule apply, `required` fields must all be there, and the version field of [optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.html) must match the document. ## SQL tables - Replace by id exists only on MongoDB (`Replace by id API is not supported for instance type …` otherwise). On a SQL table, [update by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.html) with every column does the same. ## From your code [`g.sys.db.replaceById`](https://docs.apimaker.dev/v4/examples/sys/db/replaceById.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mongodb/inventory/customers/replace-by-id/:id[/:primaryKey]` | | API id | `GEN_PUT_REPLACE_BY_ID` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.replaceByIdGen`](https://docs.apimaker.dev/v4/examples/sys/db/replaceById.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Remove by ID API > Delete one row by its primary key or any column with the remove by id API of API Maker, and get the removed row back, with select and deep. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-id-api.html Deletes one row and answers with the row as it was, so the caller can show or log what disappeared. | | | |---|---| | Method | DELETE | | URL | `/api/schema/admin/mysql8/inventory/customers/:id[/:primaryKey]` | | Body | none | | Query params | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.html), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html), throwErrorIfRecordNotFound, find | | Answer | `data` : the removed row, or `null` when no row matched | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no (a write ; it resets the cache of the table) | | From code | [`g.sys.db.removeById`](https://docs.apimaker.dev/v4/examples/sys/db/removeById.html) | | API id | `SCHEMA_DEL_DELETE_BY_ID` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/:id[/:primaryKey]` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-id-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/:id[/:primaryKey]` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The call ```text DELETE /api/schema/admin/mysql8/inventory/customers/1 ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "customer_id": 1, "first_name": "Bob", "last_name": "lin", "pincode": 382345, "isActive": 1 } } ``` - The id is the primary key. `DELETE /api/schema/admin/mysql8/inventory/customers/Bob/first_name` removes the first row whose `first_name` is `Bob`. ## Options - `?select=first_name` and `?deep=[…]` shape the returned row (deep populates it before it is removed). - `?throwErrorIfRecordNotFound=true` answers `404` and `Record not found.` for an unknown id ; the default is `data: null`. - `?find={isActive:0}` adds a condition : the row is removed only when it matches. A pre hook can add such a condition to keep callers to their own rows. ## After the call - The cache of the table is reset, WebSocket subscribers are notified, post hooks receive the removed row in `g.res.output`. - To remove many rows at once, use [remove by query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-query-api.html). ## From your code [`g.sys.db.removeById`](https://docs.apimaker.dev/v4/examples/sys/db/removeById.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/:id[/:primaryKey]` | | API id | `GEN_DEL_DELETE_BY_ID` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.removeByIdGen`](https://docs.apimaker.dev/v4/examples/sys/db/removeById.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Query for Get Data API > Read rows with a JSON query in the body with the query API of API Maker - find with operators, find and join across tables, select, sort, paging, deep populate and total count. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-api.html The read API for queries too long or too private for a URL : the same `find`, `select`, `sort`, `skip`, `limit`, `deep` and `getTotalCount` as get all, in the body. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/query` | | Body | `{ find, select, sort, skip, limit, deep, getTotalCount }` | | Query params | none | | Answer | `data` : an array of rows, `totalCount` with `getTotalCount: true` | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, with `enableCaching` on the table | | From code | [`g.sys.db.query`](https://docs.apimaker.dev/v4/examples/sys/db/query.html) | | API id | `SCHEMA_POST_QUERY` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/query` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/query` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The body | Key | Meaning | |---|---| | `find` | Required. The filter, with every operator of [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html). `{}` for everything. | | `select` | Fields to return : `"first_name,last_name"`, `"-password"`, or `{ first_name: 1 }`. | | `sort` | `"-last_update,first_name"` or `{ last_update: -1 }`. | | `skip`, `limit` | Paging. | | `deep` | Related rows, see [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html). | | `getTotalCount` | `true` adds `totalCount`, the number of rows matching `find` before paging. | ```text POST /api/schema/admin/mysql8/inventory/customers/query ``` **Body** ```json { "find": { "isActive": 1, "pincode": { "$gte": 382345 } }, "select": "customer_id,first_name,last_name", "sort": "-customer_id", "skip": 0, "limit": 10, "getTotalCount": true } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 5, "first_name": "Eve", "last_name": "Page" } ], "totalCount": 5 } ``` ## Operators `find` takes every query operator of MongoDB, and API Maker translates them to SQL where a table can answer them. | Operator | Example | Meaning | |---|---|---| | `$eq`, `$ne` | `{ "first_name": { "$ne": "Bob" } }` | equal, not equal (a plain value means `$eq`, `null` asks for the rows without a value) | | `$gt`, `$gte`, `$lt`, `$lte` | `{ "pincode": { "$gte": 382345, "$lt": 382349 } }` | comparisons on numbers, strings and ISO dates | | `$in`, `$nin` | `{ "customer_id": { "$in": [ 1, 2 ] } }` | in a list, not in a list (an array) | | `$and`, `$or`, `$nor` | `{ "$or": [ { "customer_id": 1 }, { "pincode": 382345 } ] }` | every, any, none of the conditions (an array), inside each other as deep as needed | | `$not` | `{ "customer_id": { "$not": { "$gt": 3 } } }` | negate the operators | | `$exists` | `{ "last_update": { "$exists": false } }` | with or without the field (`IS NULL` on SQL) | | `$regex`, `$options` | `{ "first_name": { "$regex": "^ma", "$options": "i" } }` | regular expression | | `$mod` | `{ "customer_id": { "$mod": [ 2, 0 ] } }` | remainder of a division | | `$expr` | `{ "$expr": { "$gt": [ "$spent", "$budget" ] } }` | compare fields with each other | | `$like` | `{ "first_name": { "$like": "Ma%" } }` | SQL like pattern, `%` and `_` | | `$isNull` | `{ "last_update": { "$isNull": true } }` | null (or not null with `false`), SQL databases | - Several operators on one field are all required, on every database : `{ "pincode": { "$gte": 382345, "$lt": 382349 } }`. - `$type`, `$all`, `$elemMatch`, `$size`, `$text`, `$where`, `$jsonSchema` and the geospatial operators are for MongoDB instances ; the bitwise operators work on every database. - See [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html#operators) for every operator, what each one becomes in SQL, and how `null` and regular expressions behave per database. ## Find and join A dotted key in `find` filters on a field of a **related** table, across databases and instances : **Orders of gold customers : orders.customer_id refers to customers** ```json { "find": { "customer_id.loyalty_tier": "GOLD" } } ``` - The relation comes from the schema of the table, as many levels as the dots : `"customer_id.city_id.name": "Pune"`. A `deep` item of the call does not make one, and a virtual field can not be used. - The rows of the related table are read through their own query API, with the permissions of the caller. - See [find and join](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html#find-and-join-fields-of-related-tables) for every rule. ## Deep populate ```json { "find": {}, "limit": 5, "deep": [ { "s_key": "customer_id", "t_col": "orders", "t_key": "customer_id", "isMultiple": true, "select": "order_no,total", "deep": [ { "s_key": "shipping_id", "t_col": "shippings", "t_key": "id" } ] } ] } ``` - Two items in `deep` populate two fields ; `deep` inside an item goes one level further. With relations in the schema, `{ "s_key": "customer_id" }` is enough. ## Types in filters - The values of `find` are converted to the types of the schema before the query : `"customer_id": "8"` matches the number 8. ## Caching - With caching on for the table, the same body answers from Redis until a write changes the table. `x-am-cache-control: reset_cache` forces the database. ## From your code [`g.sys.db.query`](https://docs.apimaker.dev/v4/examples/sys/db/query.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/query` | | API id | `GEN_POST_QUERY` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.queryGen`](https://docs.apimaker.dev/v4/examples/sys/db/query.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Query for Get Data by Stream API > Stream the rows of a JSON query with the query by stream API of API Maker - the body of the query API, the answer of get all by stream, for exports and big result sets. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-by-stream-api.html The body of [query for get data](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-api.html) with the answer of [get all by stream](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.html) : rows are sent as the database produces them. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/query-stream` | | Body | `{ find, select, sort, skip, limit, deep, getTotalCount }` | | Query params | none | | Answer | `data` : an array of rows, streamed | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no : streams always come from the database | | From code | [`g.sys.db.queryByStream`](https://docs.apimaker.dev/v4/examples/sys/db/queryByStream.html) | | API id | `SCHEMA_POST_QUERY_STREAM` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/query-stream` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-by-stream-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/query-stream` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The call ```text POST /api/schema/admin/mysql8/inventory/customers/query-stream ``` **Body** ```json { "find": { "isActive": 1 }, "sort": "customer_id", "select": "customer_id,first_name" } ``` **Answer, written in chunks** ```json { "data": [ { "customer_id": 1, "first_name": "Bob" }, { "customer_id": 2, "first_name": "Alice" } ], "success": true, "statusCode": 200, "meta": {} } ``` - Same keys and operators as the query API, find and join and deep included. Read `success` at the end : an error after the first rows closes the answer with `"success": false` and the errors. - Never cached ; post hooks can not change what was sent. ## From your code **TypeScript** ```typescript const rows: any[] = []; await g.sys.db.queryByStream({ instance: 'mysql8', database: 'inventory', collection: 'customers', find: { isActive: 1 }, select: 'customer_id,first_name', }, (row) => rows.push(row)); ``` ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/query-stream` | | API id | `GEN_POST_QUERY_STREAM` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.queryByStreamGen`](https://docs.apimaker.dev/v4/examples/sys/db/queryByStream.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-by-stream-api.md) --- # Remove by Query API > Delete every row matching a filter with the remove by query API of API Maker - a find in the body, the removed rows, their count and their ids in the answer. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-query-api.html Deletes every row matching `find` and answers with the rows removed, their ids and their count. A body without `find` is refused : nothing is ever deleted without a filter. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/query/delete` | | Body | `{ find, select? }` | | Query params | none | | Answer | `data` : `{ deletedRows, deletedRowsCount, ids }` | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no (a write ; it resets the cache of the table) | | From code | [`g.sys.db.removeByQuery`](https://docs.apimaker.dev/v4/examples/sys/db/removeByQuery.html) | | API id | `SCHEMA_POST_QUERY_DELETE` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/query/delete` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-query-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/query/delete` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The call ```text POST /api/schema/admin/mysql8/inventory/customers/query/delete ``` **Body** ```json { "find": { "isActive": 0 } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "deletedRows": [ { "customer_id": 7, "first_name": "Old", "last_name": "Row", "isActive": 0 } ], "deletedRowsCount": 1, "ids": [ 7 ] } } ``` - `find` takes every operator of [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html), including dotted keys of related tables. `{}` matches everything. - The rows are read before the delete, so `deletedRows` holds them as they were ; `select` keeps only some of their fields. ## More filters ```json { "find": { "customer_id": { "$in": [ 2, 3, 4 ] } } } ``` ```json { "find": { "$and": [ { "isActive": 0 }, { "last_update": { "$lt": "2022-01-01T00:00:00.000Z" } } ] } } ``` **the orders of one customer : orders.customer_id refers to customers** ```json { "find": { "customer_id.email": "gone@example.com" } } ``` ## Missing find ```json { "success": false, "statusCode": 400, "errors": [ { "code": 400, "message": "Please provide 'find' property having query to remove data." } ] } ``` ## After the call - The cache of the table is reset and WebSocket subscribers are notified once. Post hooks receive the answer in `g.res.output`. ## From your code [`g.sys.db.removeByQuery`](https://docs.apimaker.dev/v4/examples/sys/db/removeByQuery.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/query/delete` | | API id | `GEN_POST_QUERY_DELETE` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.removeByQueryGen`](https://docs.apimaker.dev/v4/examples/sys/db/removeByQuery.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Aggregate API > Run a MongoDB aggregation pipeline through the schema aggregate API of API Maker - $match, $group, $project, $addFields, $bucket, $facet, $lookup, $count and the other stages, with the permissions of the caller. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-aggregate-api.html Runs a MongoDB aggregation pipeline on a collection : the body is the array of stages, the answer their result. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mongodb/inventory/customers/aggregate` | | Body | the pipeline : an array of stages | | Query params | none | | Answer | `data` : the documents produced by the pipeline | | Databases | MongoDB only | | Cached | yes, with `enableCaching` on the table | | From code | [`g.sys.db.aggregate`](https://docs.apimaker.dev/v4/examples/sys/db/aggregate.html) | | API id | `SCHEMA_POST_AGGREGATE` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mongodb/inventory/customers/aggregate` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-aggregate-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mongodb/inventory/customers/aggregate` : replace `admin` with the user path of your account, `mongodb` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The call ```text POST /api/schema/admin/mongodb/inventory/customers/aggregate ``` **Body : orders per customer, the biggest first** ```json [ { "$match": { "status": "PAID" } }, { "$group": { "_id": "$customer_id", "orders": { "$sum": 1 }, "total": { "$sum": "$total" } } }, { "$sort": { "total": -1 } }, { "$limit": 10 } ] ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "_id": 1, "orders": 4, "total": 51600 } ] } ``` - A body which is not an array is refused with `Please provide array for aggregate body.` ## Stages Every stage of the MongoDB version you run is accepted. The ones used most : | Stage | Example | |---|---| | `$match` | `{ "$match": { "pincode": { "$gt": 380000 } } }` | | `$group` | `{ "$group": { "_id": "$pincode", "count": { "$sum": 1 } } }` | | `$project` | `{ "$project": { "name": { "$toLower": "$first_name" } } }` | | `$addFields` | `{ "$addFields": { "full_name": { "$concat": [ "$first_name", " ", "$last_name" ] } } }` | | `$sort`, `$skip`, `$limit` | `{ "$sort": { "total": -1 } }` | | `$count` | `{ "$count": "customers" }` | | `$bucket` | `{ "$bucket": { "groupBy": "$price", "boundaries": [ 0, 100, 500 ], "default": "Other", "output": { "count": { "$sum": 1 } } } }` | | `$facet` | several pipelines in one : `{ "$facet": { "byPrice": [ … ], "byCategory": [ … ] } }` | | `$lookup` | a join inside MongoDB : `{ "$lookup": { "from": "orders", "localField": "customer_id", "foreignField": "customer_id", "as": "orders" } }` | | `$unwind` | `{ "$unwind": "$orders" }` | | `$collStats` | `{ "$collStats": { "storageStats": {}, "count": {} } }` | ## With a schema - The `find` of a first `$match` stage is converted with the schema (types, ObjectIds, dotted keys of relations), so `{ "$match": { "customer_id.loyalty_tier": "GOLD" } }` filters through the table `customer_id` refers to, as a find and join. ## Good to know - Only MongoDB : SQL databases answer `This API is only supported for mongodb.` For SQL, run a query with [executeQuery](https://docs.apimaker.dev/v4/examples/sys/system/executeQuery.html) from a custom API. - Field permissions of the groups apply to what a stage returns. A caller who can not read a field does not get it, projected or not. - With caching on, a pipeline answers from Redis until the collection changes. ## From your code [`g.sys.db.aggregate`](https://docs.apimaker.dev/v4/examples/sys/db/aggregate.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mongodb/inventory/customers/aggregate` | | API id | `GEN_POST_AGGREGATE` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.aggregateGen`](https://docs.apimaker.dev/v4/examples/sys/db/aggregate.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Count API > Count the rows matching a filter with the count API of API Maker - a find in the body, a number in the answer, on every database. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-count-api.html Answers how many rows match `find`, without reading them. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/count` | | Body | `{ find }` | | Query params | none | | Answer | `data` : the number of rows | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, with `enableCaching` on the table | | From code | [`g.sys.db.count`](https://docs.apimaker.dev/v4/examples/sys/db/count.html) | | API id | `SCHEMA_POST_COUNT` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/count` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-count-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/count` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The call ```text POST /api/schema/admin/mysql8/inventory/customers/count ``` **Body** ```json { "find": { "isActive": 1 } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": 5 } ``` - `find` takes every operator of [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html) : `$gt`, `$in`, `$and`, `$or`, `$like`, dotted keys of related tables… `{ "find": {} }` counts the table. ## Examples ```json { "find": { "pincode": { "$gte": 382345 } } } ``` ```json { "find": { "$or": [ { "first_name": "Bob" }, { "first_name": "Alice" } ] } } ``` ```json { "find": { "first_name": { "$like": "%all%" } } } ``` **orders of gold customers : orders.customer_id refers to customers** ```json { "find": { "customer_id.loyalty_tier": "GOLD" } } ``` ## Types in the filter - The values are converted with the schema : `"pincode": "100050"` counts the number 100050. ## Good to know - For a list with its total in one call, [query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-api.html) with `getTotalCount: true` saves the second request. - With caching on, the count answers from Redis until a write changes the table. ## From your code [`g.sys.db.count`](https://docs.apimaker.dev/v4/examples/sys/db/count.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/count` | | API id | `GEN_POST_COUNT` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.countGen`](https://docs.apimaker.dev/v4/examples/sys/db/count.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Distinct API > List the distinct values of one or several columns with the distinct API of API Maker - fields and their order in the path, an optional find filter, on every database. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-api.html Lists the distinct values of a column, or the distinct combinations of several, sorted the way you ask. | | | |---|---| | Method | GET | | URL | `/api/schema/admin/mysql8/inventory/customers/distinct/:field[/:order]` | | Body | none | | Query params | [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.html) | | Answer | `data` : an array of objects, one per distinct value or combination | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, with `enableCaching` on the table | | From code | [`g.sys.db.distinct`](https://docs.apimaker.dev/v4/examples/sys/db/distinct.html) | | API id | `SCHEMA_GET_DISTINCT` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/distinct/:field[/:order]` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/distinct/:field[/:order]` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## One column ```text GET /api/schema/admin/mysql8/inventory/customers/distinct/pincode ``` ```json { "success": true, "statusCode": 200, "data": [ { "pincode": 382345 }, { "pincode": 382346 }, { "pincode": 382347 } ] } ``` - Ascending by default. Descending : `/api/schema/admin/mysql8/inventory/customers/distinct/pincode/desc` (also `-1`, `dsc`, `0`, `false` ; ascending is `asc`, `1`, `true`). ## Several columns ```text GET /api/schema/admin/mysql8/inventory/customers/distinct/first_name,pincode GET /api/schema/admin/mysql8/inventory/customers/distinct/first_name,pincode/asc,desc ``` ```json { "success": true, "statusCode": 200, "data": [ { "first_name": "Alice", "pincode": 382346 }, { "first_name": "Bob", "pincode": 382345 }, { "first_name": "Eve", "pincode": 382349 }, { "first_name": "Eve", "pincode": 382348 } ] } ``` - One order per column, in the same position. A missing order is ascending. ## With a filter ```text GET /api/schema/admin/mysql8/inventory/customers/distinct/pincode?find={isActive:1} ``` - The same `find` as get all. For a filter in the body, use [distinct with query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-with-query-api.html). ## Good to know - A column which is empty everywhere gives `[]`. - Field permissions apply : a column the group can not read can not be listed. ## From your code [`g.sys.db.distinct`](https://docs.apimaker.dev/v4/examples/sys/db/distinct.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/distinct/:field[/:order]` | | API id | `GEN_GET_DISTINCT` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.distinctGen`](https://docs.apimaker.dev/v4/examples/sys/db/distinct.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Distinct with Query API > List the distinct values of columns for the rows matching a find in the body with the distinct with query API of API Maker. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-with-query-api.html [Distinct](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-api.html) with the filter in the body : the distinct values of a column among the rows matching `find`. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/distinct/:field[/:order]` | | Body | `{ find }` | | Query params | none | | Answer | `data` : an array of objects, one per distinct value or combination | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, with `enableCaching` on the table | | From code | [`g.sys.db.distinctQuery`](https://docs.apimaker.dev/v4/examples/sys/db/distinctQuery.html) | | API id | `SCHEMA_POST_DISTINCT_QUERY` (groups, settings, hooks, WebSocket subscriptions) | | Without a schema | `/api/gen/admin/mysql8/inventory/customers/distinct/:field[/:order]` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-with-query-api.html#without-a-schema) | > **Generated (`/api/gen`) or schema (`/api/schema`) ?** > > Both families offer the same operations on the same URL pattern. The **schema APIs** read the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of the table : values are converted to their types, validated, encrypted, defaulted and numbered before they reach the database. The **generated APIs** are schemaless : they pass your data as it is, so a number sent as `"22"` stays a string. Use the schema APIs for the tables your application writes to, and the generated APIs for quick reads and for tables without a schema. ## The URL `/api/schema/admin/mysql8/inventory/customers/distinct/:field[/:order]` : replace `admin` with the user path of your account, `mysql8` with your instance, `inventory` with the database and `customers` with the table. The examples use this table : | customer_id | first_name | last_name | last_update | pincode | isActive | |-------------|------------|-----------|-----------------------|---------|----------| | 1 | Bob | lin | 2022-11-14 04: 34: 58 | 382345 | 1 | | 2 | Alice | Page | 2022-10-15 02: 10: 40 | 382346 | 1 | | 3 | Mallory | Brown | 2022-09-13 03: 44: 05 | 382347 | 1 | | 4 | Eve | Mathly | 2022-11-12 01: 59: 33 | 382348 | 1 | | 5 | Eve | Page | 2022-11-12 01: 59: 33 | 382349 | 1 | ## The call ```text POST /api/schema/admin/mysql8/inventory/customers/distinct/customer_id/asc ``` **Body** ```json { "find": { "pincode": { "$in": [ 382345, 382346 ] } } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 1 }, { "customer_id": 2 } ] } ``` - Fields and orders in the path work as in [distinct](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-api.html) : `first_name,pincode/asc,desc`. - `{ "find": {} }` lists every distinct value. ## Examples ```json { "find": { "pincode": { "$gte": 382347 } } } ``` ```json { "find": { "$or": [ { "pincode": 382345 }, { "customer_id": 4 } ] } } ``` ```json { "find": { "first_name": { "$like": "%llory" } } } ``` ## Types in the filter - The values of `find` are converted with the schema : `"pincode": "382350"` matches the number. ## From your code [`g.sys.db.distinctQuery`](https://docs.apimaker.dev/v4/examples/sys/db/distinctQuery.html) makes the same call from custom APIs, hooks, events, schedulers and test cases, with the same headers and params. To get the whole [response envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of `data`, pass `true` as the second argument in TypeScript, or call it through `g.sys.db.full` in the other languages. ## Without a schema : /api/gen Every table also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mysql8/inventory/customers/distinct/:field[/:order]` | | API id | `GEN_POST_DISTINCT_QUERY` (groups, settings, hooks, WebSocket subscriptions) | | From code | [`g.sys.db.gen.distinctQueryGen`](https://docs.apimaker.dev/v4/examples/sys/db/distinctQuery.html#without-a-schema) | The data passes as it is : nothing is converted, validated, defaulted, encrypted or numbered, and a filter matches the type you send (`"22"` does not match the number `22`). On MongoDB, strings which look like ObjectIds are converted. The query params, headers and errors are the ones of this page. ## Headers Every call takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) : the tokens, `x-am-response-case`, `x-am-content-type-response` (JSON, XML, YAML…), `x-am-response-object-type: make_flat`, `x-am-cache-control`, `x-am-get-encrypted-data`, `x-am-internationalization`, `x-am-tenant-username`, `x-am-meta`. ## Errors | Code | When | |---|---| | `401` | The token in `x-am-authorization` is missing or invalid. | | `403` | No group of the API user grants this API of this table, or a field of the request. | | `404` | The instance, database or table of the URL does not exist, or the table has no schema (use `/api/gen`). | | `400` | The query or the body is wrong : the [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) say which key and why. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) and [post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) : what the conversions and validations do to every write. --- # Custom APIs > Write a function in API Maker, in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin, and it is live as an API - path and method, settings for caching, access, validation, files and timeouts, the global object g to reach your databases, file uploads and downloads, any content type, hooks, versions and tests. Source: https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html A custom API is a function behind a path and a method, written in any of the [languages](https://docs.apimaker.dev/v4/docs/languages/index.html) of API Maker. Save it in the admin panel (`APIs & Logic → Custom API`) or in your editor through the local client, and it answers at `/api/custom-api//` right away, in a sandbox, with the whole of API Maker available through the [global object `g`](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html). | | | |---|---| | URL | `/api/custom-api//` | | Method | the `requestMethod` of the settings : `GET`, `POST`, `PUT` or `DELETE` | | Body, query, params, headers, files | `g.req.body`, `g.req.query`, `g.req.params`, `g.req.headers`, `g.req.body.files` | | Answer | what `main` returns, in `data` of the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html), or a file, or any content type | | Language | TypeScript on Node.js or Bun, Python, PHP, C#, Go, Rust, Java or Kotlin : picked above the editor, `codeLanguage` of the API | | Runs in | the sandbox of the account for its language ; the machine of the server with `runOnNativeProcess: true` | | Access | `apiAccessType` : `TOKEN_ACCESS` (default), `IS_PUBLIC`, `NO_ACCESS` (only from other code) | | From code | `g.sys.system.callExternalApi` over HTTP, or [`g.sys.test.runCustomApi`](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html) in a test | ## Hello world Every custom API has two files : the **basic info** (its settings) and the **code**. The code is written in any [language](https://docs.apimaker.dev/v4/docs/languages/index.html) ; the basic info is always TypeScript. **Basic info** ```typescript import * as T from 'types'; let customApi: T.ICustomApiSettingsTypes = { name: 'Hello World', path: '/hello-world', requestMethod: T.ERequestMethod.GET, apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, errorList: [], }; module.exports = customApi; ``` **TypeScript** **Code** ```typescript import * as T from 'types'; import * as db from 'db-interfaces'; async function main(g: T.IAMGlobal) { return { hello: g.req.query.name || 'world' }; } module.exports = main; ``` ```bash curl "$AM/api/custom-api/admin/hello-world?name=Bob" -H "x-am-authorization: $TOKEN" ``` ```json { "success": true, "statusCode": 200, "data": { "hello": "Bob" } } ``` Pick the language above the editor, and the editor offers its template. The list of the custom APIs shows the language of each with its logo. The hooks of a custom API are written in [the language of its code](https://docs.apimaker.dev/v4/docs/languages/index.html#one-language). - The path and the method must match exactly : `/hello-world` with `GET`. Variables go in the query string or the body, not in the path. - `path` + `requestMethod` is unique in the account ; `name` is unique too and names the folder in Git. ## The settings | Key | Meaning | |---|---| | `name`, `path`, `requestMethod` | Identity and URL. | | `apiAccessType` | `TOKEN_ACCESS` : the API user token, plus the person token when `authProviders` names one. `IS_PUBLIC` : no token. `NO_ACCESS` : not reachable over HTTP, only from other code and the testing page. | | `authProviders` | Names of the [auth providers](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) whose token the person must send. Absent : the providers of the default secret. `[]` : the API user token only. | | `enableCaching` | Cache the answer in Redis, per URL, body and headers that matter, until `resetCacheOnModificationOf` says otherwise or the [reset custom API cache](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.html) system API runs. | | `resetCacheOnModificationOf` | What resets the cache : `'DB:instance:database:table'` when that table is written through API Maker, `'CA:name'` when that custom API is called. | | `acceptOnlyEncryptedData` | Refuse plain bodies : the body must be an [encrypted payload](https://docs.apimaker.dev/v4/docs/features/security-features.html#encrypted-request-payloads). | | `reqBodySchema`, `reqQueryParametersSchema` | A [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) for the body and for the query params : converted and validated before the code runs, every error at once with `400`. | | `errorList` | The messages your API throws, listed so they can be translated with [i18n](https://docs.apimaker.dev/v4/docs/i18/i18.html) and shown in the docs of the API. | | `runOnNativeProcess` | Run on the machine of the server instead of in the sandbox : the packages of the folder API Maker runs in and the programs of the machine, no isolation. Use with care. | | `customApiTimeoutInSeconds` | How long the code and the hooks of the API may run, in seconds, wherever they run (the sandbox, `runOnNativeProcess`). Empty : `sandboxReqTimeout` of the configuration (13 seconds). A request which sends `x-am-sandbox-timeout` is given that time instead. | | `separateSandboxSettings` | A sandbox of its own for this API, with its own runtime, version, packages and count : [separate sandbox](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html#separate-sandbox). | | `fileUpload` | `enable`, `allowFileUploadFields`, and per field `minFileSizeBytes`, `maxFileSizeBytes`, `allowedExtensionsArr`. | | `swaggerDocs` | What the Swagger document of the API users shows for this API. | See [Custom API settings](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html) for every key with its example. ## Validate the request with a schema **Basic info** ```typescript let customApi: T.ICustomApiSettingsTypes = { name: 'Create Order', path: '/orders', requestMethod: T.ERequestMethod.POST, errorList: [], reqBodySchema: { customer_id: { __type: T.EType.number, validations: { required: true } }, items: [ { product_id: { __type: T.EType.string, validations: { required: true } }, qty: { __type: T.EType.number, validations: { min: 1 } } } ], }, reqQueryParametersSchema: { dryRun: { __type: T.EType.boolean, conversions: { defaults: { defaultValue: false } } }, }, }; ``` - The body and the query params reach your code converted (types, trims, defaults) and checked (required, min, enum…). A wrong request never reaches the code. - The [is valid data for custom API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-custom-api.html) system API runs the same check without calling the API. ## Reach your data and the rest of API Maker **TypeScript** ```typescript import * as T from 'types'; import * as db from 'db-interfaces'; async function main(g: T.IAMGlobal) { const person = g.req.auth.authAMDB; // the signed-in person const orders = await g.sys.db.query({ instance: 'mongodb', database: 'shop', collection: 'orders', find: { customer_id: person.customer_id, status: 'PAID' }, sort: '-created_at', limit: 10, deep: [ { s_key: 'items' } ], }); const total = orders.reduce((sum, order) => sum + order.amount, 0); g.logger.log(`${orders.length} orders`); // reaches the log table and the caller's logs await g.sys.cache.setKey(`orders:${person.customer_id}`, JSON.stringify(orders), 60); return { orders, total }; } module.exports = main; ``` - In TypeScript `await` every `g.sys` call (a coroutine of Python and a task of C# too) ; the other languages wait for the answer of each call. The whole [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) instead of an error comes with `true` as the second argument in TypeScript, and through `full` in the other languages (`g.sys.db.full.query`). - In TypeScript, `import * as db from 'db-interfaces'` gives the interfaces generated from your schemas, `db...I
` ; the editors of the other languages offer the same columns on the rows a call answers. - Code shared by many APIs (a price, a rule) goes into a [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html). - Every method of `g` has a page in the [code examples](https://docs.apimaker.dev/v4/examples/index.html). ## Errors and status codes **TypeScript** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { if (!g.req.body?.customer_id) { throw [{ code: T.EStatusCode.BAD_REQUEST, message: 'Please provide customer_id.' }]; // 400 with the message in errors } if (g.req.body.customer_id < 0) { g.res.statusCode = T.EStatusCode.BAD_REQUEST; // 400 with your data return { field: 'customer_id', message: 'A customer_id is positive.' }; } return { ok: true }; } module.exports = main; ``` - An error raised answers `success: false` with its message in `errors`, and its code as the status : `throw [{ code, message }]` in TypeScript, `raise Errors(message, code=400)` in Python, `AmErrors` in PHP, C#, Java and Kotlin, `am.Fail(400, message)` in Go, `am::fail(400, message)` in Rust. Any other exception answers `500` with its message. A message listed in `errorList` is translated by i18n. - `g.res.statusCode` sets the status of a normal answer ; `g.res.warnings` adds warnings. ## Files **Upload.** Send `multipart/form-data` with the files in the field `files` (or `files1`, `files2`… up to `files41` to keep groups apart). They are on disk when the code runs, in `g.req.body.files` : **TypeScript** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const files: { originalname: string; mimetype: string; size: number; path: string; filename: string }[] = g.req.body.files || []; return files.map(f => ({ name: f.originalname, bytes: f.size })); } module.exports = main; ``` - `fileUpload.validations.files.maxFileSizeBytes` and `allowedExtensionsArr` in the settings refuse a wrong file before the code runs. Uploaded files are cleaned from the uploads folder after `uploadedFileRemoveOlderThanThisTimeInSeconds` (30 minutes by default). **Download.** Return an object with the `__am__` keys of `IDownloadResponse` : the file (or a zip of several) is streamed to the caller. **TypeScript** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { // a file written earlier in the uploads folder of the sandbox return { __am__downloadFilePath: 'reports/2026-09.pdf', __am__downloadFolderFileName: 'September report.pdf', __am__cleanupFileOrFolderPaths: 'reports/2026-09.pdf', }; } module.exports = main; ``` - `__am__downloadFileOrFolderPaths` with several paths, or `{ fsSource, archiveDestination }` objects, answers a zip. ## Any content type Set `g.res.contentType` and return the content : HTML, text, XML, YAML, or bytes as base64 for any other MIME type. **TypeScript** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { g.res.contentType = T.EContentType.HTML; return '

Hello

'; } module.exports = main; ``` **TypeScript** **A tiny gif** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { g.res.contentType = 'image/gif' as T.EContentType; // not in EContentType : the return is base64 return 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA='; } module.exports = main; ``` - `g.res.headers` adds response headers. See [Content types](https://docs.apimaker.dev/v4/examples/res/res.html#content-types). ## Around the code > Diagram : The life of a request : app → Caddy → token and groups → pre hooks → the API → post hooks → reply > > The life of a request in API Maker : the app calls through Caddy, API Maker checks the token and the groups, runs the pre hooks, the API itself against the database or the cache, the post hooks, and replies in JSON, XML or YAML. - **Hooks** : pre and post hooks on the custom API itself, see [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html). - **Events and WebSockets** : an event can trigger automatically after the API, and WebSocket clients subscribed to the API get its answer. - **Versions** : several versions of the code, one active ; switch back in one click. - **Docs** tab : notes for the people who use the API, shown in the API testing page and the Swagger of the API users. - **Test cases** : [`g.sys.test.runCustomApi`](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#testing-a-custom-api) runs the code with mocks and measures its coverage. - **Git** : `src/custom_apis//` holds the basic info (`.config.ts`), the code (`.ts`, or `.py`, `.go`… in another language) and the docs. - **Groups** : an API user calls a custom API only when a group grants it, see [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html). ## Native process `runOnNativeProcess: true` runs the function on the machine of the server instead of in Docker, in one runner of its runtime shared by every account : no sandbox start, the packages installed in the folder API Maker runs in, and the programs of the machine, such as a browser for [Playwright](https://docs.apimaker.dev/v4/docs/guides/browser-automation.html). The runtime must be installed on the server (`node`, `python3`, `php`…), in a [version API Maker runs](https://docs.apimaker.dev/v4/docs/languages/index.html#versions) : [API Maker Launchpad](https://docs.apimaker.dev/v4/docs/getting-started/local-run.html#languages-for-native-apis) installs it for you, on your computer and on your servers, or finds the one they have. The code is not isolated from the server, so keep it for the few APIs which need it. That runner loads API Maker itself, as a sandbox does ([`g.sys` is answered where the code runs](https://docs.apimaker.dev/v4/docs/languages/index.html#g-sys-in-the-sandbox)) : `g.sys.db.getAll(…)`, a key of the cache, a secret are function calls in the process of the runner, which reads the databases and Redis itself, for the account of each request. Only the answer of the API goes back to the server. This holds in every language ; a release has the libraries it needs, built for its machine. Where none is found for the machine of the server (a server built by hand on a Mac : `AM_NATIVE_CORE` in the [configuration](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html#process-environment)), the server answers those calls itself, as it says in its log. ## Samples in every account A new account comes with custom APIs under `/default/…` which show the features at work : deep populate, find and join, caching, a WebSocket emitter, S3 upload and download, a login which returns both tokens, a captcha, data isolation with hooks. Open them on the Custom API page and read the code. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.md) --- # Custom API Settings > The Basic Info of a custom API in API Maker - name, path and method, access type and auth providers, caching with reset rules, request schemas, timeout, native process, error list, file upload rules and Swagger docs. Source: https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html The **Basic Info** tab of a custom API is a TypeScript object : where the API answers, who may call it, and how it behaves. It is always TypeScript, whatever [language](https://docs.apimaker.dev/v4/docs/languages/index.html#basic-info) the code of the API is written in, and API Maker [runs it itself](https://docs.apimaker.dev/v4/docs/languages/index.html#settings-run) when you save : the tab shows TypeScript with a lock. | | | |---|---| | Page | `APIs & Logic → Custom API` → the API → **Basic Info**. | | Type | `T.ICustomApiSettingsTypes` | | URL | `/api/custom-api/` with `requestMethod`. Path and method together are unique. | **Basic Info** ```typescript import * as T from 'types'; import { EType } from 'types'; let customApi: T.ICustomApiSettingsTypes = { name: 'Create Customer', // unique ; the folder in Git requestMethod: T.ERequestMethod.POST, path: '/customers', enableCaching: false, resetCacheOnModificationOf: [ // 'DB:mongodb:shop:customers', // a write to this table resets the cache of this API // 'CA:other_custom_api', // a call of that custom API resets it ], acceptOnlyEncryptedData: false, // customApiTimeoutInSeconds: 10, errorList: [ 'Unable to process this request' ], // translatable in i18n apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, // authProviders: ['users_tg'], reqBodySchema: { first_name: { __type: EType.string, validations: { required: true } }, phone: { __type: EType.number, validations: { min: 5 } }, }, fileUpload: { enable: false, allowFileUploadFields: [ T.EFilesVariables.files ], validations: {} }, }; module.exports = customApi; ``` ## The keys | Key | Meaning | |---|---| | `name` | Unique name, also the folder of the API in Git. | | `path`, `requestMethod` | Where it answers : `GET`, `POST`, `PUT` or `DELETE`. `/customers/:id` gives `g.req.params.id`. | | `apiAccessType` | `TOKEN_ACCESS` (default), `IS_PUBLIC`, or `NO_ACCESS` for an API only your code calls. | | `authProviders` | The person tokens the call must carry. Absent : `common.authProviders` of the secret ; `[]` : the API user token alone. | | `enableCaching` | Cache the answer in Redis per body, query and person. | | `resetCacheOnModificationOf` | `DB:instance:database:table`, `CA:custom API name` : what drops the cached answers. Otherwise [reset custom API cache](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.html). | | `acceptOnlyEncryptedData` | Refuse plain payloads : [encrypted payloads](https://docs.apimaker.dev/v4/docs/features/security-features.html#encrypted-request-payloads). | | `reqBodySchema`, `reqQueryParametersSchema` | A [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) for the body and the query : validated and converted before the code runs, checkable with [is valid data for custom API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-custom-api.html). | | `customApiTimeoutInSeconds` | How long the code of the API and the hooks of its versions may run, in seconds, in the sandbox and with `runOnNativeProcess` alike. Empty : `sandboxReqTimeout` (13 seconds). A request which sends [`x-am-sandbox-timeout`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-sandbox-timeout) is given that time instead : [time of a run](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#time-of-a-run). | | `runOnNativeProcess` | Run on the process of API Maker instead of the sandbox : faster, less isolated ; `console.log` is not captured, use `g.logger`. | | `errorList` | The messages this API throws, listed for [internationalization](https://docs.apimaker.dev/v4/docs/i18/i18.html). | | `fileUpload` | `enable`, `allowFileUploadFields` (`files`…), and per field `validations` : `minFileSizeBytes`, `maxFileSizeBytes`, `allowedExtensionsArr`. See [files](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html#files). | | `swaggerDocs` | `tag`, `summary`, `description`, `parameters` for the Swagger docs of the API users. | | `separateSandboxSettings` | Sandboxes of their own for this API : see [below](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html#separate-sandbox). | The language of the code of the API is chosen in the title bar of its editor, not here : see [languages](https://docs.apimaker.dev/v4/docs/languages/index.html). ## Separate sandbox By default the code of a custom API runs in the sandboxes of its language, with the packages of the [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html). An API which needs other packages, another runtime or version, or which must not share its sandboxes with the rest, names a group : every API of the account which names the same group shares its sandboxes, and no other code runs there. **Reports in a sandbox of their own, on Bun** ```typescript let customApi: T.ICustomApiSettingsTypes = { name: 'Monthly report', requestMethod: T.ERequestMethod.GET, path: '/reports/monthly', apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, separateSandboxSettings: { enableSeparateSandboxForThis: 'reports', // the group runtime: 'bun', // another runtime of the language of the code version: '1.4', sandboxCount: 1, packages: { allowAllPackagesOfAdmin: true, // the packages of the Sandbox Settings, and : sandboxPackages: [{ name: 'pdfkit', version: '0.15.0' }], }, // dockerFileOfSandbox: 'FROM {{AM_BASE_IMAGE}}\nRUN apt-get … \n{{AM_PACKAGES}}\n{{AM_RUNNER}}', }, }; ``` | Key | Meaning | |---|---| | `enableSeparateSandboxForThis` | The name of the group. Empty : the sandboxes of the language. | | `runtime`, `version` | A runtime of the language of the code (`node` or `bun` for TypeScript) and a tag of its image. The ones of the Sandbox Settings when absent. | | `sandboxCount` | The most sandboxes of the group at a time, on each server. | | `packages.sandboxPackages` | The packages of the group, from the registry of its language. | | `packages.allowAllPackagesOfAdmin` | `true` : the packages of the Sandbox Settings too. A package named in both is installed once, in the version of the group. | | `dockerFileOfSandbox` | A [Dockerfile](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#dockerfile) of the group, with the same markers. | - The group gets an image of its own (shared with any account which has the same settings), built when the API is saved. - Changing the settings of a group starts its sandboxes anew on every server. ## Related - [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html) · [Auth of a database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) --- # Pre Hooks > Run your code (TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin) before any API of API Maker - at the level of an instance, a database, a table, one API, a custom or system API, or a group - to check, change, narrow or answer a request, in the order API Maker runs them. Source: https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html A pre hook is a function of yours, in any of the [languages](https://docs.apimaker.dev/v4/docs/languages/index.html) 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. > Diagram : 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 > > Order of the hooks around a database API : the pre hooks of the instance, then of the database, then of the table, then of the API run before it ; the API runs ; the post hooks run in the opposite order, API, table, database, instance. A pre hook which returns a value answers the caller and stops everything after it. ## 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](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html). - 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](https://docs.apimaker.dev/v4/docs/Git/git.html#hooks), 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](https://docs.apimaker.dev/v4/docs/languages/index.html#one-language) : 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 **TypeScript** ```typescript 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; ``` 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](https://docs.apimaker.dev/v4/docs/languages/index.html#in-the-server) 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](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html), 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](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) 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. > Diagram : The row gate : the same call, without and with the row scoping pre hook > > Without a row scoping pre hook, Diya calling get all on orders gets every order of the table, hers and Kabir's. With the hook, API Maker adds her customer id to the filter with $and before the query runs, and she gets only her two orders. **TypeScript** **Collection level pre hook on orders** ```typescript 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([ '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([ '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([ '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; ``` The report writes this hook for you, for the tables which need it, and reads it again on every scan. [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html) builds the same pattern for several tables at once, with a group which reads every row. ## Answer without the API **TypeScript** **A pre hook which answers from the cache** ```typescript 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; ``` ## Validate and change **TypeScript** ```typescript 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; ``` ## Utility classes in a hook A hook calls your [utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) like any other code of its language : a check shared by many hooks lives in one. **TypeScript** **utils/Guards : a utility class** ```typescript 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** ```typescript 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; ``` **A pre hook which uses it** ```python from Guards import require_role async def main(g): require_role(g, 'PURCHASE_INVOICE_VIEW') ``` **A pre hook which uses it** ```php Main(G g) { Guards.RequireRole(g, "PURCHASE_INVOICE_VIEW"); return null; } } ``` **A pre hook which uses it** ```go 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") } ``` **A pre hook which uses it** ```rust use am::{Errors, G, Value}; use crate::Guards; pub fn main(g: &mut G) -> Result { Guards::require_role(g, "PURCHASE_INVOICE_VIEW")?; Ok(Value::Null) } ``` **A pre hook which uses it** ```java import am.G; class Api { static Object main(G g) { Guards.requireRole(g, "PURCHASE_INVOICE_VIEW"); return null; } } ``` **A pre hook which uses it** ```kotlin import am.G fun main(g: G): Any? { Guards.requireRole(g, "PURCHASE_INVOICE_VIEW") return null } ``` - Press Ctrl+Space in the editor for the list of your [utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html). ## 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. - [Post hooks →](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) - [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.md) --- # Post Hooks > Run your code (TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin) after any API of API Maker - reshape the answer in g.res.output, notify, log, or replace it - at the level of an instance, a database, a table, one API or a custom or system API, in the reverse order of the pre hooks. Source: https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html A post hook is a function of yours, in any of the [languages](https://docs.apimaker.dev/v4/docs/languages/index.html) of API Maker, which runs **after** an API, with its result in `g.res.output`. It can reshape the answer, add to it, replace it, send something somewhere, or refuse to answer at all. It lives at the same levels as a [pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) : instance, database, table, API, custom and system API, with optional groups. > Diagram : 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 > > Order of the hooks around a database API : the pre hooks of the instance, then of the database, then of the table, then of the API run before it ; the API runs ; the post hooks run in the opposite order, API, table, database, instance. A pre hook which returns a value answers the caller and stops everything after it. ## Order Post hooks run from the narrowest level to the widest : the hooks of the API, then of the table, then of the database, then of the instance, each list top to bottom. The last one to run sees what every other one did. In API Testing, the **Post Hooks** tab of an API of a table lists every post hook which runs after it, in that order : its own, then the hooks of its table, its database and its instance (marked **Table**, **Database**, **Instance** : they are changed in their categories of API Testing). In Git, every hook is a [folder of its own](https://docs.apimaker.dev/v4/docs/Git/git.html#hooks). A post hook is written in the language of its API, as every hook is : the code of an API and all its hooks are [one language on one runtime](https://docs.apimaker.dev/v4/docs/languages/index.html#one-language). ## The code **TypeScript** ```typescript import * as T from 'types'; import * as db from 'db-interfaces'; async function main(g: T.IAMGlobal) { if (!g.req.isApiRequestFromUser) return; // g.res.output : the answer of the API (data), changeable // g.res.statusCode, g.res.warnings, g.res.errors // g.req : the request as the API saw it, after the pre hooks // g.res.shared : what the pre hooks left for you } module.exports = main; ``` | The hook | Effect | |---|---| | returns nothing | The next post hook runs, then the answer is sent. | | changes `g.res.output` | The answer is what is in `g.res.output` at the end. | | returns a value | That value becomes the answer and the remaining post hooks do not run. | | throws | The request answers with the error, whatever the API did. The database change of the API is **not** rolled back by that. | - A hook in any language other than TypeScript runs in the sandbox of its language. A hook in TypeScript runs [inside API Maker](https://docs.apimaker.dev/v4/docs/languages/index.html#in-the-server) 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. - No hook runs when the answer comes from the [cache](https://docs.apimaker.dev/v4/docs/features/automatic-caching.html). Calls from your own code skip hooks unless `skipHookRunning: false`. ## Reshape the answer **TypeScript** **Hide a field and add a computed one** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { if (!g.req.isApiRequestFromUser) return; const rows = Array.isArray(g.res.output) ? g.res.output : [ g.res.output ]; for (const row of rows) { if (!row) continue; delete row.password; row.amount_with_tax = Math.round(row.amount * 1.18); } } module.exports = main; ``` ## Do something after a write **TypeScript** **Audit row and an event after a save** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { if (!g.req.isApiRequestFromUser) return; const now = Date.now(); // milliseconds since 1970 await g.sys.db.saveSingleOrMultiple({ instance: 'mongodb', database: 'shop', collection: 'audit_logs', saveData: { api: g.req.reqInfo.apiInfo.id, by: g.req.auth.authAMDB?.email, at: now, took: now - g.res.shared.startedAt }, }); await g.sys.system.emitEvent('order-placed', g.res.output); } module.exports = main; ``` - For a notification to WebSocket clients, nothing to write : subscribers of the table are notified after every successful call. Emit a custom WebSocket event with `g.sys.system.emitEventWS` when the notification needs another shape. - For an event after an API hit without code, attach the event to the API on the [Events](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.html) page (automatic trigger). ## Streams On get all by stream and query by stream the rows are already on their way when the post hooks run : `g.res.output` only holds the last chunk, and a value returned or thrown can not change what was sent. Reshape a stream in a custom API around `g.sys.db.getAllByStream` instead. ## Good to know - WebSocket notifications and automatic events run after the post hooks, on the final answer. A post hook which throws still lets the notification go out ; a pre hook which throws does not. - Post hooks have versions and a Test button on the API testing page, like pre hooks, and deploy with Git. - [← Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) - [Events](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.md) --- # Encrypt Data API > Encrypt any value with the encrypt data system API of API Maker, using the transfer key of your secret or a key of your own, and get a string a client or a later call can decrypt. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-encrypt-data-api.html Encrypts a string, a number, an object or an array and answers the encrypted text. By default it uses `encryptionAlgorithmFETransfer` and `secretFETransfer` of the default [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html) : the key you share with your frontend or mobile app, so the two sides can exchange encrypted data. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/encrypt-data` : `admin` is the user path of your account | | Body | `{ data, algorithm?, pass? }` | | Answer | `data` : the encrypted string | | From code | [`g.sys.system.encrypt`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-encrypt-data-api.html#from-code) | ## The call **Body** ```json { "data": { "name": "Joseph", "card": "4111 1111 1111 1111" } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": "U2FsdGVkX19iIBA3FQy3OgFXeE4B2cC8lAfaJjCbMmUllceqb58YwvQqU33PkQEQ" } ``` | Key | Meaning | |---|---| | `data` | Required. Any JSON value. It is serialised before encryption, so [decrypt](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-decrypt-data-api.html) gives the same type back. | | `algorithm` | `AES` (default from the secret). `RC4` and `TRIPLEDES` are legacy ciphers, kept for the data of API Maker 3. | | `pass` | Your own key, instead of `secretFETransfer` of the secret. | - The keys come from the default secret of the account, unless the body gives its own `algorithm` and `pass`. ## From code **TypeScript** ```typescript const text = await g.sys.system.encrypt({ name: 'Joseph' }); // with the transfer key of the secret const mine = await g.sys.system.encrypt('hello', T.EEncryptionAlgorithm.AES, 'my-own-key'); // with the algorithm and the key given ``` ## Good to know - Fields of a table are encrypted at rest with `conversions: { encryption: true }` in the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html), with the `secret` key of the secret, not this API. - To send an encrypted request body, see [encrypted payloads](https://docs.apimaker.dev/v4/docs/features/security-features.html#encrypted-request-payloads) ; to get an encrypted answer, the header [`x-am-get-encrypted-data`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-get-encrypted-data). ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key, for example an unknown `algorithm`. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-encrypt-data-api.md) --- # Decrypt Data API > Decrypt a string made by the encrypt data API, or by a client with the transfer key of your secret, with the decrypt data system API of API Maker. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-decrypt-data-api.html Decrypts what [encrypt data](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-encrypt-data-api.html) produced, or what a client encrypted with `encryptionAlgorithmFETransfer` and `secretFETransfer` of the secret, and gives the original value back. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/decrypt-data` : `admin` is the user path of your account | | Body | `{ data, algorithm?, pass? }` | | Answer | `data` : the decrypted value, parsed as JSON when it is JSON | | From code | [`g.sys.system.decrypt`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-decrypt-data-api.html#from-code) | ## The call **Body** ```json { "data": "U2FsdGVkX19iIBA3FQy3OgFXeE4B2cC8lAfaJjCbMmUllceqb58YwvQqU33PkQEQ" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "name": "Joseph", "card": "4111 1111 1111 1111" } } ``` - `algorithm` and `pass` work as on encrypt data : give the same ones the text was encrypted with. - A text which does not decrypt with the key answers `data: null`. ## From code **TypeScript** ```typescript const encryptedText = g.req.body.encrypted; const value = await g.sys.system.decrypt(encryptedText); // with the transfer key of the secret const mine = await g.sys.system.decrypt(encryptedText, T.EEncryptionAlgorithm.AES, 'my-own-key'); // with the algorithm and the key given ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-decrypt-data-api.md) --- # Hash Data API > Hash a value with the hash data system API of API Maker - HMAC SHA-256 with the nonce or secret of your secret, the same hash the hashing conversion of a schema stores, so you can look a hashed column up. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-hash-data-api.html Answers the SHA-256 hash of the body, computed with the `nonce` (or, without one, the `secret`) of the default secret. A string is hashed as it is, exactly like the `hashing` conversion of a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) hashes a field, so the hash of a password or a tax id given here equals the one stored in the table and can be used to find the row. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/hash-data` : `admin` is the user path of your account | | Body | the value to hash : a string, or any JSON | | Answer | `data` : the hash, a hex string | | From code | [`g.sys.system.hash`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-hash-data-api.html#look-up-a-hashed-column) | ## The call **Body : a string** ```json "Joseph" ``` **Body : an object (hashed as its JSON text)** ```json { "name": "Joseph" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": "be2a6a457cc36a48e4e231411d8d6498213f7ffb6d1e03066dd7fbaa6ec9402e" } ``` - The algorithm is `hashingAlgorithm` of the secret : `SHA256` is the one supported. - The same value always gives the same hash for the same secret, which is the point : hash a value on the way in and compare hashes on the way out. ## Look up a hashed column **TypeScript** **Find the customer whose tax id (hashed in the table) is known** ```typescript const taxId = g.req.body.taxId; const digest = await g.sys.system.hash(taxId); const suppliers = await g.sys.db.query({ instance: 'mongodb', database: 'shop', collection: 'suppliers', find: { tax_id_hash: digest } }); ``` - Keep an encrypted copy of the value in another field when you also need to read it back : encryption is reversible, hashing is not. ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-hash-data-api.md) --- # Get Token API > Get the token of an API user or of a person from API Maker with the token API - username and password, or the name of a token generator, refresh tokens, several tokens in one call, and tenant tokens. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html Gives the two tokens every call may need : the token of an **API user** (which application calls, sent in `x-am-authorization`) and the token of a **person**, a row of your own users table read by a DB token generator (sent in `x-am-user-authorization`). See [the two gates](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.html#the-two-gates). | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/token` : `admin` is the user path of your account | | Body | `{ u, p, expiresInSeconds? }`, `{ name, u, p }`, `{ refresh_token, name? }`, or an array of them | | Answer | `data` : `{ token, refresh_token, expires_in }`, or an array | | From code | [`g.sys.system.getToken`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html#from-code) | ## The token of an API user **Body : without name, an API user of API Maker** ```json { "u": "default", "p": "12345", "expiresInSeconds": 259200 } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "token": "eyJhbGciOi…", "refresh_token": "eyJhbGciOi…", "expires_in": 259200 } } ``` - `expiresInSeconds` is optional : **Tokens Expire After** of the [Deployment Settings](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html#security-apis) otherwise (72 hours by default). - The password of an API user lives in the secret, under the path set on the API user (`common.apiUserPasswords.default` for the API user of a new account). ## The token of a person **Body : with the name of a token generator** ```json { "name": "users_tg", "u": "alice@acme.com", "p": "PASSWORD" } ``` - The generator (an [auth provider](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) of type DB) knows the users table and its username and password columns. A hashed password column works : the password given is hashed and compared. - The groups of the person come from the groups column of the generator. ## Refresh ```json { "name": "users_tg", "refresh_token": "eyJhbGciOi…" } ``` - `refresh_token` of an earlier answer gives a new token without the password. For a person, add the `name` of the generator. A refresh token is valid `refreshTokenValidForS` seconds (900 by default) after the token expired. ## Several tokens in one call ```json [ { "u": "default", "p": "12345" }, { "name": "users_tg", "u": "alice@acme.com", "p": "PASSWORD" } ] ``` - The answer is an array in the same order. The sample custom API `/default/login` of a new account does exactly this and returns both tokens to the app. ## The token of a tenant user ```text POST /api/system-api/admin/token x-am-tenant-username: acme { "name": "users_tg", "u": "alice@acme.com", "p": "PASSWORD" } ``` - When the generator reads a users table of a [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) instance, name the tenant : the person is read from the database of that tenant, and the token works for that tenant only, also after a refresh. ## From code `g.sys.system.getToken` makes the same call from your code, whatever the settings of the token API say about HTTP callers. ### The token of an API user - Token of an API Maker API user: no `name`. **TypeScript** ```typescript const apiUser = await g.sys.system.getToken({ "u": "USER_NAME", "p": "USER_PASSWORD" }); // apiUser.token goes in x-am-authorization ``` ### The token of a person - Token of a database user (application user): the `name` of its token generator. **TypeScript** ```typescript const databaseUser = await g.sys.system.getToken({ "name": "TOKEN_GENERATOR_NAME", "u": "USER_NAME", "p": "PASSWORD" }); // databaseUser.token goes in x-am-user-authorization, databaseUser.refresh_token gets a new one later ``` ### Both in one call - Both at once: an array in, an array of tokens out. **TypeScript** ```typescript const [apiUser, databaseUser] = await g.sys.system.getToken([ { "u": "USER_NAME", "p": "USER_PASSWORD" }, { "name": "TOKEN_GENERATOR_NAME", "u": "USER_NAME", "p": "PASSWORD" } ]); // apiUser.token goes in x-am-authorization, databaseUser.token in x-am-user-authorization ``` ## Who may call it - The token API is public by default : anybody can ask for a token with a username and a password. - When its [system API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) list auth providers, it needs the tokens those settings ask for, like any other API. - A token is a JWT signed with `passJWT` of the server. Changing the password of the API user, or `passwordChangedAtColumn` of a person, invalidates the tokens made before. ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. The token API itself is public unless its settings say otherwise. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key, `Please provide token request with ['u', 'p'] or ['refresh_token'].` for an incomplete body. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.md) --- # Call External API > Call any HTTP API from API Maker with the call external API system API - JSON, forms, file uploads, SOAP and XML, bytes, with basic, bearer, API key and OAuth 2 authentication, redirects, retries, file downloads, and lists of calls in parallel or in sequence. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html Makes HTTP calls to any API from the server : REST with JSON, a form, an upload of files, SOAP and other XML, GraphQL, bytes. It says who asks (basic, bearer, API key, OAuth 2), follows redirects, tries again when a call failed, and gives the answer as JSON, as text, as bytes, or as a file it saved. One call, or a list of calls run in parallel or one after the other, with `preProcess` and `postProcess` rules which copy a value of one answer into the request of another. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/call-external-api` : `admin` is the user path of your account | | Body | one call, an array of calls, or `{ type: "parallel" | "sequential", data: [ … ] }` | | Answer | `data` : the response of the call, `{ statusCode, ok, headers, body, … }`, or an array of them. The file itself, for one call with [`download`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#download-a-file) | | From code | [`g.sys.system.callExternalApi`](https://docs.apimaker.dev/v4/examples/sys/system/callExternalApi.html) | ## One call **Body** ```json { "url": "https://api.example.com/customers", "method": "POST", "timeout": 5000, "headers": { "authorization": "Bearer …" }, "queryParams": { "select": "first_name" }, "body": { "first_name": "James", "last_name": "Bond" } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "statusCode": 201, "ok": true, "headers": { "content-type": "application/json" }, "body": { "id": 42, "first_name": "James" } } } ``` | Key | Meaning | |---|---| | `url` | Required. The full URL. | | `method` | `GET` (default), `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. | | `timeout` | Milliseconds one request may take. 10 minutes when it is not given. | | `headers` | The headers of the request. A name is the same header whatever its case. | | `queryParams`, `pathParams`, `queryArrayFormat` | The values of the query and of the path : see [the URL](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#the-url). | | `body`, `bodyType` | What to send, and in which form : see [the body](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#the-body). | | `auth` | Who asks : see [authentication](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#authentication). | | `responseType` | How the body of the answer is given : see [the answer](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#the-answer). | | `download` | Saves the body of the answer as a file : see [download a file](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#download-a-file). | | `followRedirects`, `retry`, `throwOnError`, `allowFailure`, `maxResponseSize` | See [redirects, retries and failures](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#redirects-retries-and-failures). | | `id` | A name for this call, used by `preProcess` and `postProcess` of other calls. | | `preProcess`, `postProcess` | Rules `{ from, to }` run before and after the call : `from` is a path starting with the `id` of a call (`saveApi.output.body.data.id`), `to` a path in a call (`setApi.body.customerId`). | | `output` | Filled with the answer of the call, for the rules of the others. | - A call with only the keys of the first example works as it always did : every other key is optional. ## The URL **Body : the values of the path and of the query** ```json { "url": "https://{region}.api.example.com/customers/:customerId/orders", "pathParams": { "region": "eu", "customerId": 42 }, "queryParams": { "status": ["paid", "sent"], "page": 2 }, "queryArrayFormat": "repeat" } ``` - This calls `https://eu.api.example.com/customers/42/orders?status=paid&status=sent&page=2`. - `pathParams` fills `{name}` anywhere before the query, and `:name` where a part of the path starts. Each value is written as a part of a path : `a/b` is `a%2Fb`. A name without a value stays as it is written. - `queryParams` are added after the ones the URL has already. A text, a number and a boolean are sent as they are, an object as the text of its JSON. - `queryArrayFormat` says how a list is written : | `queryArrayFormat` | `{ "ids": [1, 2] }` is sent as | |---|---| | `json` (default) | `ids=[1,2]` | | `repeat` | `ids=1&ids=2` | | `brackets` | `ids[]=1&ids[]=2` | | `comma` | `ids=1,2` | ## Authentication `auth` says who asks. It takes the place of an `authorization` header of the call. | `auth` | What is sent | |---|---| | `{ "type": "basic", "username": "…", "password": "…" }` | `Authorization: Basic …` | | `{ "type": "bearer", "token": "…" }` | `Authorization: Bearer …`. `prefix` replaces the word `Bearer`. | | `{ "type": "apiKey", "name": "x-api-key", "value": "…" }` | The header `x-api-key`. With `"in": "query"`, the query parameter `x-api-key`. | | `{ "type": "oauth2", "tokenUrl": "…", "clientId": "…", "clientSecret": "…", "scope": "…" }` | `Authorization: Bearer `, with a token asked of `tokenUrl`. | **Body : OAuth 2 client credentials** ```json { "url": "https://api.example.com/reports", "auth": { "type": "oauth2", "tokenUrl": "https://login.example.com/oauth/token", "clientId": "CLIENT_ID", "clientSecret": "CLIENT_SECRET", "scope": "reports.read" } } ``` - The token of `oauth2` is asked for with the `client_credentials` grant, kept in memory until 30 seconds before it expires, and used by every call with the same `auth`. When the API answers `401`, a new token is asked for and the call is sent again, once. - `grantType` names another grant, and `params` what else the token request holds : `{ "grantType": "password", "params": { "username": "…", "password": "…" } }`. `audience` is sent when it is given. `"credentialsIn": "header"` sends the id and the secret of the client as `Authorization: Basic` and not in the form. - Keep the secrets out of your code : read them with [`g.sys.system.getSecret`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-secret-by-name-api.html#from-code), or copy the token of a call into the next one with `postProcess` (`"to": "ordersApi.auth.token"`). ## The body `bodyType` says in which form the body is sent, and gives the request its `content-type` when the call names none. | `bodyType` | `body` | `content-type` | |---|---|---| | `json` | An object or an array, sent as JSON. A text is sent as it is. | `application/json` | | `form` | An object : `{ "a": 1, "b": "x y" }` is sent as `a=1&b=x+y`. A list is its key once for each item, an object the text of its JSON. | `application/x-www-form-urlencoded` | | `multipart` | An object : every key is a part of the form, a field or a [file](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#files). | `multipart/form-data`, with its boundary | | `text` | A text. | `text/plain; charset=utf-8` | | `xml` | A text. | `application/xml; charset=utf-8` | | `binary` | Bytes : a `Buffer`, their Base64 text, or a [file](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#files). | `application/octet-stream`, or the one the name of the file says | - Without `bodyType`, an object is sent as JSON, or as a form when the `content-type` header of the call is `application/x-www-form-urlencoded` or `multipart/form-data`. A text is sent as it is, a `Buffer` as its bytes. - A `GET` or a `HEAD` without a `body` is sent without one. Every other JSON call without a `body` sends `{}`. **Body : a form** ```json { "url": "https://api.example.com/oauth/token", "method": "POST", "bodyType": "form", "body": { "grant_type": "refresh_token", "refresh_token": "…" } } ``` **Body : SOAP** ```json { "url": "https://soap.example.com/service", "method": "POST", "bodyType": "xml", "headers": { "content-type": "text/xml; charset=utf-8", "SOAPAction": "urn:GetRate" }, "body": "", "responseType": "text" } ``` **Body : GraphQL** ```json { "url": "https://api.example.com/graphql", "method": "POST", "auth": { "type": "bearer", "token": "…" }, "body": { "query": "query ($id: ID!) { customer(id: $id) { name } }", "variables": { "id": 42 } } } ``` ### Files A file is a value of a `multipart` body, or the whole `binary` body. | Value | The file | |---|---| | `{ "file": "reports/a.pdf" }` | A file of the uploads folder (`uploads/reports/a.pdf` in a sandbox). `fileName` and `contentType` replace its name and what the end of its name says it is. | | `{ "content": "…", "fileName": "note.txt" }` | A text sent as a file. With `"encoding": "base64"`, the bytes the Base64 text stands for. Without `fileName` it is a field, with the `contentType` you give it. | | A file of `g.req.files` | A file a form sent to your custom API, sent on under its own name and content type. | | A `Buffer` | Bytes made by your code. | **Body : a form of several parts, with two files** ```json { "url": "https://api.example.com/documents", "method": "POST", "bodyType": "multipart", "body": { "title": "Contract", "tags": ["signed", "2026"], "document": { "file": "contracts/42.pdf" }, "note": { "content": "Signed on site", "fileName": "note.txt" } } } ``` **Body : the bytes of a file as the whole body** ```json { "url": "https://storage.example.com/bucket/42.pdf", "method": "PUT", "bodyType": "binary", "body": { "file": "contracts/42.pdf" } } ``` - Only files of the uploads folder are read : a path which leads out of it is refused. - A list as the value of a key sends one part for each item, under the same name. An object which is no file is sent as a field of JSON. - The body of a call is read whole before it is sent : 128 MB at most. ## The answer | Key | Meaning | |---|---| | `statusCode` | The HTTP status of the answer. | | `ok` | `true` when the status is 200 to 299. | | `headers` | The headers of the answer, their names in small letters. | | `body` | The body of the answer : see `responseType`. `null` for a `download` which was saved. | | `url`, `redirected` | When redirects are followed : the URL the answer came from, and whether a redirect was followed. | | `attempts` | With `retry` : how many times the request was sent. | | `file` | With `download` : the file which was saved, `{ path, name, size, contentType }`. | | `error` | With `allowFailure` : why the call failed, `{ message, code }`. | | `responseType` | `body` | |---|---| | `auto` (default) | JSON when the body is JSON, else its text. Bytes when it is no text : a picture, a PDF, an archive. | | `json` | JSON. A body which is no JSON is an error which shows its start. | | `text` | The text of the body, whatever it holds. | | `base64` | The bytes of the body, as Base64 text. | | `buffer` | The bytes of the body : a `Buffer` in JavaScript and TypeScript, Base64 text in the other languages and over HTTP. | - A text is read in the character set its `content-type` names (`charset=iso-8859-1`, `windows-1256`, `shift_jis`, `utf-16` …), UTF-8 without one. - A body which came compressed (`content-encoding: gzip`, `deflate`, `br`) is unpacked. - A status of 400 or above is an answer as any other, unless the call asks for [`throwOnError`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.html#redirects-retries-and-failures). ## Download a file `download` saves the body of the answer as a file of the uploads folder, piece by piece as it arrives where the server itself sends the request, and answers with `file`. The body is not kept in memory, and `body` is `null`. | `download` | Where the file goes | |---|---| | `true` | A place of its own in the uploads folder, with the ending of the file : `3f0c…9a.pdf`. | | `"invoices/42.pdf"` | That place in the uploads folder. A file which is there gives way. | | `"invoices/"` | That folder, under the name of the file. | | `{ "path": "invoices/42.pdf", "fileName": "Invoice 42.pdf" }` | The place, and the name the file has for who it is sent on to. | **Answer of a call with download** ```json { "statusCode": 200, "ok": true, "headers": { "content-type": "application/pdf" }, "body": null, "url": "https://cdn.example.com/invoices/42.pdf", "redirected": true, "file": { "path": "invoices/42.pdf", "name": "invoice-42.pdf", "size": 48213, "contentType": "application/pdf" } } ``` - `file.name` is the name the server gives the file (`content-disposition`), else the last part of its URL, else `download`. - Redirects are followed, 10 at most, unless the call says `"followRedirects": false`. - An answer whose status is not 200 to 299 is no file : it is given as any other answer, with its `statusCode` and its `body`, and without `file`. - A file can be 2 GB ; `maxResponseSize` names another limit, in bytes. - **From code**, the file is in the uploads folder of the sandbox : read it at `uploads/` + `file.path`, or send it to the caller of your custom API by returning [`__am__downloadFilePath`](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html#files). A file which is not sent on stays until the uploads are cleaned (`uploadedFileRemoveOlderThanThisTimeInSeconds`, 30 minutes by default). - **Over HTTP**, the answer of one call with `download` is the file itself, with its `content-type` and `content-disposition: attachment; filename="…"`, and the file is removed from the server once it was sent. A list of calls answers JSON, and leaves its files in the uploads folder. **Download a file through API Maker** ```bash curl -X POST 'https://example.com/api/system-api/admin/call-external-api' \ -H 'x-am-authorization: TOKEN' -H 'content-type: application/json' \ -d '{ "url": "https://api.example.com/invoices/42/pdf", "download": true }' \ --output invoice.pdf ``` ## Redirects, retries and failures | Key | Meaning | |---|---| | `followRedirects` | `true` follows 10 redirects at most, a number that many. Default : `false`, the redirect is the answer. As a browser does : after a `303`, and after a `301` or a `302` of a `POST`, the request is a `GET` without a body. Another server is not sent the `authorization` and `cookie` headers. | | `retry` | Sends the request again when it failed : a number of tries after the first one (10 at most), or the object below. | | `throwOnError` | `true` : a status of 400 or above is thrown as an error with the status as its `code`, and the `statusCode`, `headers` and `body` of the answer. | | `allowFailure` | `true` : a call which failed is not thrown. Its answer is `{ "statusCode": 0, "ok": false, "error": { "message": "…" } }`, and the calls after it run. | | `maxResponseSize` | The most bytes of the answer. 64 MB when it is not given, 2 GB for a `download`. | | `timeout` | Milliseconds one request may take : each try has that long. | | Key of `retry` | Default | Meaning | |---|---|---| | `count` | `3` | How many times the request is sent again. | | `delay` | `300` | Milliseconds before the second try. | | `factor` | `2` | What the wait is multiplied by from one try to the next. | | `maxDelay` | `30000` | The longest wait, in milliseconds. | | `statusCodes` | `[408, 429, 500, 502, 503, 504]` | The statuses which are tried again. | | `onNetworkError` | `true` | Whether a request which got no answer is tried again : no connection, a timeout. | **Body : a call which is tried again, and thrown when it still fails** ```json { "url": "https://api.example.com/payments", "method": "POST", "body": { "amount": 1200 }, "retry": { "count": 4, "delay": 500, "statusCodes": [429, 503] }, "throwOnError": true } ``` - A `retry-after` header of the answer is waited for, up to `maxDelay`. - When every try failed, the answer is the last one. `attempts` says how many times the request was sent. - A `POST` which is sent again can be done twice by the API it calls : name the statuses which are safe to try again, or send a key the API recognises a repeated request by. ## In parallel, in sequence **Body : a save, then two calls at once with its result** ```json [ { "id": "saveApi", "url": "https://api.example.com/customers", "method": "POST", "body": { "first_name": "James" }, "postProcess": [ { "from": "saveApi.output.body.id", "to": "setApi.body.customerId" } ] }, { "type": "parallel", "data": [ { "id": "setApi", "url": "https://api.example.com/preferences", "method": "PUT", "body": {} }, { "id": "notify", "url": "https://hooks.example.com/new-customer", "method": "POST", "body": { "source": "shop" }, "allowFailure": true } ] } ] ``` - The top level array runs in sequence ; `type: "parallel"` runs its `data` at once, `type: "sequential"` one after the other. Groups nest. - The answer is an array with one entry per call, nested like the body. - In sequence, a call which fails ends the list, unless it has `allowFailure`. In parallel, the place of a call which failed is empty, or holds its `error` with `allowFailure`. - A rule can fill any key of a call : `"to": "ordersApi.auth.token"`, `"to": "ordersApi.pathParams.customerId"`. ## From code **TypeScript** ```typescript const resp = await g.sys.system.callExternalApi<{ id: number }>({ url: 'https://api.example.com/customers/42' }); if (resp.statusCode === 200) g.logger.log(resp.body.id); ``` A custom API which downloads an invoice from another server and sends it to its own caller : **TypeScript** ```typescript const saved = await g.sys.system.callExternalApi({ url: 'https://api.example.com/invoices/42/pdf', auth: { type: 'bearer', token: 'TOKEN' }, download: true, throwOnError: true, }); // the file is in the uploads folder now : your code reads it at 'uploads/' + saved.file.path return { __am__downloadFilePath: saved.file.path, __am__downloadFolderFileName: saved.file.name }; ``` - More examples in every language : [forms, files, XML, OAuth 2, retries and downloads](https://docs.apimaker.dev/v4/examples/sys/system/callExternalApi.html). - Custom APIs can also use any HTTP client of npm from the sandbox ; this API adds the forms, the files, the authentication, the retries, the parallel and sequential plans and the data copying without code. ## Where the request leaves from - Called over HTTP, the request leaves from the server of API Maker. - Called from your code, it leaves from where the code runs : its sandbox. The names and the addresses the sandbox reaches are the ones the call reaches. - The files a call sends and saves are the ones of the uploads folder, which the server and the sandboxes share. ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API : a pre hook which adds the `auth` of the call keeps the secrets of the other server out of the reach of the caller. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key (`Invalid value for bodyType. Allowed values are [json, form, multipart, text, xml, binary].`), a file which is not in the uploads folder, or a `download` which leads out of it. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | | The status of the other server | With `throwOnError` : `POST https://api.example.com/payments answered with the status 503.` A status API Maker does not answer with itself is answered as `500`, with the status in the `code` of the error. | | `500` | The request got no answer, or its answer can not be given : `connect ECONNREFUSED 10.0.0.5:443`, `getaddrinfo ENOTFOUND api.example.com`, `timeout of 5000ms exceeded`, `length limit exceeded`, `Too many redirects : 10 were followed, …`, `The answer is no JSON : …`. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) · [Files of custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html#files) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.md) --- # Execute Plain Query API > Run a SQL statement or a MongoDB command on any instance of the account with the execute plain query system API of API Maker - DDL, DML, joins, stored procedures, database commands - with the permissions of the caller. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-execute-plain-query-api.html Runs what the generated APIs can not : a SQL statement as text on a SQL instance, or a database command on MongoDB, and answers what the driver returns. It is the tool of [migration scripts](https://docs.apimaker.dev/v4/docs/features/database-migration.html) and of the few custom APIs which need a join, a stored procedure or a DDL statement. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/execute-plain-query` : `admin` is the user path of your account | | Body | `{ instance, database?, collection?, query }` | | Answer | `data` : the result of the driver (rows, or the result of the command) | | From code | [`g.sys.system.executeQuery`](https://docs.apimaker.dev/v4/examples/sys/system/executeQuery.html) | ## The call **Body : SQL** ```json { "instance": "mysql8", "query": "SELECT status, COUNT(*) AS orders FROM inventory.orders GROUP BY status" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "status": "PAID", "orders": 128 }, { "status": "NEW", "orders": 7 } ] } ``` **Body : a MongoDB command** ```json { "instance": "mongodb", "database": "shop", "query": { "dbStats": 1 } } ``` | Key | Meaning | |---|---| | `instance` | Required. `crm::acme` runs on the tenant `acme` of a multi-tenant instance ; in a call made for a tenant, the plain name is enough. | | `database` | Required for PostgreSQL (one connection per database) and MongoDB. Other SQL instances name the database in the statement. | | `collection` | MongoDB, for commands which need it. | | `query` | The SQL text, or the command object of MongoDB (anything `db.command()` accepts). | ## Per database | Database | Example | |---|---| | MySQL, MariaDB, TiDB, Percona | `SELECT * FROM inventory.employees;` | | SQL Server | `SELECT * FROM inventory.dbo.employees;` · `USE inventory; exec dbo.procedureName;` | | PostgreSQL | `database: "e_commerce"` and `SELECT * FROM "public"."customers";` | | Oracle | `SELECT * FROM "INVENTORY"."employees"` : no semicolon at the end, backticks around the statement in code | | MongoDB | `{ "listIndexes": "orders" }`, `{ "createIndexes": "orders", "indexes": [ … ] }`, `{ "dbStats": 1 }` | - Several statements in one call work on the SQL databases whose connection string allows it (`multipleStatements=true` on MySQL). - The [code example](https://docs.apimaker.dev/v4/examples/sys/system/executeQuery.html) shows DDL, DML, joins, stored procedures and MongoDB indexes. ## Good to know - The statement runs with the user of the connection string : it can create, alter and drop. The [APIs Security Report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) lists this API with the sensitive system APIs ; grant it to the groups which really need it. - Values from a request must be escaped by you, or better, passed through the generated APIs. For indexes, prefer [create indexes](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.html), which works the same on every database. ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key, `Unable to get instance with name …` for an unknown instance. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) --- # Get Secret API > Read one or several keys of a secret by their path with the get secret system API of API Maker - connection strings, API keys of external services, your own keys - from the default secret or a named one. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-secret-by-name-api.html Reads values of a [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html) by their path : `common.encryptionAlgorithm`, `stripe.apiKey`… One key gives its value, several give an array in the same order. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/get-secret-by-name` : `admin` is the user path of your account | | Body | `{ keys }` ; from code also `fromSecretName` | | Answer | `data` : the value, or an array of values | | From code | [`g.sys.system.getSecret`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-secret-by-name-api.html#from-code) | ## The call **Body : one key** ```json { "keys": "stripe.apiKey" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": "sk_live_…" } ``` **Body : several keys** ```json { "keys": [ "stripe.apiKey", "common.connectionString.mysql_8" ] } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ "sk_live_…", "mysql://…" ] } ``` - `keys` is the full path in the secret object, with dots. A path to an object gives the object. - Over HTTP the default secret of the account answers. From code, `fromSecretName` reads another secret of the account by its name ; over HTTP that key is ignored. ## From code **TypeScript** ```typescript const key = await g.sys.system.getSecret('stripe.apiKey'); const [ key2, cs ] = await g.sys.system.getSecret([ 'stripe.apiKey', 'common.connectionString.mysql_8' ]); const staging = await g.sys.system.getSecret('stripe.apiKey', 'staging'); // from the secret named staging ``` ## Good to know - Secrets are never in Git and never in the answer of another API : this API is the way to read them, and it is a sensitive one. Grant it to the groups which need it, or read the secret from your code only (`NO_ACCESS` custom APIs). ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-secret-by-name-api.md) --- # Get Redis Key API > Read one or several keys of your own from the Redis of API Maker with the get redis key system API - values set with set redis key, in the namespace of your account. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-redis-key-api.html Reads values you stored with [set redis key](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.html) from the external Redis of API Maker (the one which also holds the cache of the APIs). Keys are kept in a namespace of the account, so two accounts never see each other's keys. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/get-redis-key` : `admin` is the user path of your account | | Body | `{ keys }` | | Answer | `data` : the value (a string, `null` when the key is not there), or an array of values | | From code | [`g.sys.cache.getKey`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-redis-key-api.html#from-code) | ## The call **Body : one key** ```json { "keys": "session:user123" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": "{\"cart\":3}" } ``` **Body : several keys** ```json { "keys": [ "session:user123", "captcha:abc" ] } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ "{\"cart\":3}", null ] } ``` - Values are strings : `JSON.parse` what you stored as JSON. ## From code **TypeScript** ```typescript const raw = await g.sys.cache.getKey('session:user123'); const session = raw ? JSON.parse(raw) : null; ``` ### Several keys at once - Get multiple redis keys values. **TypeScript** ```typescript await g.sys.cache.getKey(["REDIS_KEY_NAME", "ANOTHER_REDIS_KEY_NAME"]); ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-redis-key-api.md) --- # Set Redis Key API > Store one or several keys of your own in the Redis of API Maker with the set redis key system API - a value, a TTL in seconds, and NX to write only when the key is new. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.html Stores values in the external Redis of API Maker, in the namespace of the account : sessions, captchas, counters, anything a later call or another server needs. Each key can expire on its own. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/set-redis-key` : `admin` is the user path of your account | | Body | an array of `{ key, value, ttl?, NX? }` | | Answer | `data` : `true` | | From code | [`g.sys.cache.setKey`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.html#from-code) | ## The call **Body** ```json [ { "key": "session:user123", "value": "{\"cart\":3}", "ttl": 3600 }, { "key": "captcha:abc", "value": "7H2K", "ttl": 180, "NX": true } ] ``` **Answer** ```json { "success": true, "statusCode": 200, "data": true } ``` | Key | Meaning | |---|---| | `key` | The name. It is stored under the namespace of the account. | | `value` | A string. Serialise objects with `JSON.stringify`. | | `ttl` | Seconds before the key expires. Without it, `redisValueExpireInSeconds` of the [configuration](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html) (7200 by default) : keys never live forever. | | `NX` | `true` : write only when the key does not exist yet. | ## From code **TypeScript** ```typescript await g.sys.cache.setKey('session:user123', JSON.stringify({ cart: 3 }), 3600); await g.sys.cache.setKey([ { key: 'captcha:abc', value: '7H2K', ttl: 180, NX: true } ]); ``` - The [Redis dashboard](https://docs.apimaker.dev/v4/docs/logs/logs-and-dashboards.html#redis) shows the keys with their TTL and lets you edit them. ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.md) --- # Remove Redis Key API > Delete one or several keys of your own from the Redis of API Maker with the remove redis key system API - sessions, captchas, temporary data - a missing key is not an error. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-remove-redis-key-api.html Deletes keys stored with [set redis key](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.html). A key which is not there is skipped without an error, so the call can run again. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/remove-redis-key` : `admin` is the user path of your account | | Body | `{ keys }` | | Answer | `data` : `true` | | From code | [`g.sys.cache.removeKey`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-remove-redis-key-api.html#from-code) | ## The call **Body : one key** ```json { "keys": [ "session:user123" ] } ``` **Body : several keys** ```json { "keys": [ "session:user123", "captcha:abc" ] } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": true } ``` - No wildcards, on purpose : a pattern could delete the cache of the APIs. Name the keys. ## From code **TypeScript** ```typescript await g.sys.cache.removeKey('session:user123'); await g.sys.cache.removeKey([ 'session:user123', 'captcha:abc' ]); ``` - To clear the cached answers of an API rather than your own keys, use the reset cache APIs : [database](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-db-api.html), [custom](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.html), [system](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-system-api.html). ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-remove-redis-key-api.md) --- # Reset Database Cache API > Drop the cached answers of the APIs of a table, a database or an instance with the reset database cache system API of API Maker, for one tenant or all of them. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-db-api.html Drops the cached answers of the generated and schema APIs of a table, of every table of a database, or of every database of an instance. Writes through API Maker do it on their own ; this API is for data changed outside API Maker, or a cache you want gone now. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/reset-redis-cache-db` : `admin` is the user path of your account | | Body | `{ instance, database?, collection? }` or an array of them | | Answer | `data` : the number of cache entries removed, or an array of numbers | | From code | [`g.sys.cache.resetCacheDB`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-db-api.html#from-code) | ## The call **Body : one table** ```json { "instance": "mysql8", "database": "inventory", "collection": "customers" } ``` **Body : every table of a database** ```json { "instance": "mysql8", "database": "inventory" } ``` **Body : several at once** ```json [ { "instance": "mysql8", "database": "inventory", "collection": "customers" }, { "instance": "mongodb", "database": "shop", "collection": "orders" } ] ``` **Answer** ```json { "success": true, "statusCode": 200, "data": 1 } ``` - `table` works in place of `collection`. - An array body answers an array of counts in the same order ; an unknown instance gives `-1` there and a `404` in `errors`. ## One tenant ```json { "instance": "crm::acme", "database": "crm", "collection": "customers" } ``` - Names the tenant after the instance : only the cached answers of `acme` go. Without a tenant, the cache of the table is reset for the structure database and every tenant. See [Multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html#caching). ## From code **TypeScript** ```typescript await g.sys.cache.resetCacheDB({ instance: 'mysql8', database: 'inventory', collection: 'customers' }); ``` - One call for one answer : the header [`x-am-cache-control: reset_cache`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-cache-control) on the read itself. ### A tenant, or every tenant - Name the tenant after the instance to reset the cache of that tenant of a [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) instance only. Without a tenant, the cache of the table is reset for its structure database and every tenant. **TypeScript** ```typescript await g.sys.cache.resetCacheDB({ instance: "crm::acme", database: "crm", collection: "customers" }); // the tenant acme await g.sys.cache.resetCacheDB({ instance: "crm", database: "crm", collection: "customers" }); // the structure database and every tenant ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-db-api.md) --- # Reset Custom API Cache API > Drop the cached answers of one or several custom APIs with the reset custom API cache system API of API Maker, for one tenant or all of them. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.html Drops the cached answers of a custom API with `enableCaching`. `resetCacheOnModificationOf` in its settings does it on its own when a table it names is written ; this API is for the other cases. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/reset-redis-cache-custom-apis` : `admin` is the user path of your account | | Body | `{ name, tenantUsername? }` or an array of them | | Answer | `data` : the number of cache entries removed | | From code | [`g.sys.cache.resetCacheCustomApis`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.html#from-code) | ## The call **Body : one custom API** ```json { "name": "customers-report" } ``` **Body : several** ```json [ { "name": "customers-report" }, { "name": "top-products" } ] ``` **Body : one tenant** ```json { "name": "customers-report", "tenantUsername": "acme" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": 1 } ``` - `name` is the name of the custom API (its settings), not its path. An unknown name gives `-1` and a `404` in `errors`. - Without `tenantUsername`, the cached answers of every tenant and of the calls without a tenant are reset. ## From code **TypeScript** ```typescript await g.sys.cache.resetCacheCustomApis('customers-report'); await g.sys.cache.resetCacheCustomApis({ name: 'customers-report', tenantUsername: 'acme' }); ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.md) --- # Reset System API Cache API > Drop the cached answers of one or several system APIs with the reset system API cache system API of API Maker, for one tenant or all of them. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-system-api.html Drops the cached answers of a system API whose [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) have `enableCaching`, for example get table meta or get secret. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/reset-redis-cache-system-apis` : `admin` is the user path of your account | | Body | `{ name, tenantUsername? }` or an array of them | | Answer | `data` : the number of cache entries removed | | From code | [`g.sys.cache.resetCacheSystemApis`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-system-api.html#from-code) | ## The call **Body** ```json { "name": "Get table meta data" } ``` **Body : several, one for a tenant** ```json [ { "name": "Get table meta data" }, { "name": "Get secret key-keys", "tenantUsername": "acme" } ] ``` **Answer** ```json { "success": true, "statusCode": 200, "data": 1 } ``` - `name` is the name shown on the System API page : Encrypt data, Decrypt data, Hash data, Get token, Call external API, Execute plain query, Get secret key-keys, Get redis key-keys, Set redis key-keys, Remove redis key-keys, Reset database cache, Reset custom apis cache, Reset system apis cache, Get table meta data, 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. An unknown name gives `-1` and a `404` in `errors`. ## From code **TypeScript** ```typescript await g.sys.cache.resetCacheSystemApis('Get table meta data'); ``` ### A tenant - The cache of a system API called for one tenant of a [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) instance, with the [x-am-tenant-username](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-tenant-username) header. Without a tenant, the cache of every tenant is reset too. **TypeScript** ```typescript await g.sys.cache.resetCacheSystemApis({ name: "SYSTEM_API_NAME", tenantUsername: "acme" }); ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-system-api.md) --- # Get Table Meta API > Read the columns of a table or the fields of a collection in one common format with the get table meta system API of API Maker - names, types, counts - on every database, for migrations and dynamic code. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-table-meta-api.html Describes a table : the name and the type of every column (or of every field found in a MongoDB collection), in the same format for every database. Migration scripts use it to check whether a column exists before adding it. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/get-table-meta` : `admin` is the user path of your account | | Body | `{ instance, database, collection }` or an array of them | | Answer | `data` : an array of fields, or an array of arrays | | From code | [`g.sys.system.getTableMeta`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-table-meta-api.html#from-code) | ## The call **Body** ```json { "instance": "mysql8", "database": "inventory", "collection": "customers" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "name": "customer_id", "path": "customer_id", "count": 24, "total_count": 24, "type": "Number", "has_duplicates": false, "probability": 1 }, { "name": "first_name", "path": "first_name", "count": 24, "total_count": 24, "type": [ "String", "Null" ], "has_duplicates": true, "probability": 1 } ] } ``` | Key | Meaning | |---|---| | `name`, `path` | The column, with its path for nested MongoDB fields. | | `type` | The type, or the list of types seen (MongoDB). | | `count`, `total_count`, `probability` | For MongoDB, in how many of the sampled documents the field appears. | | `has_duplicates` | Whether the same value appears twice in the sample. | - `table` works in place of `collection`. An array of tables answers an array of arrays, in the same order. - A table of a tenant : `"instance": "crm::acme"`, or the header `x-am-tenant-username`. ## From code **TypeScript** **A migration which adds a column once** ```typescript const meta = await g.sys.system.getTableMeta({ instance: 'mysql8', database: 'inventory', table: 'order_transactions' }); if (!meta.find(c => c.name === 'description')) { await g.sys.system.executeQuery({ instance: 'mysql8', query: 'ALTER TABLE `inventory`.`order_transactions` ADD COLUMN `description` varchar(255) NULL' }); } ``` ### A table of a tenant - `instance: "crm::acme"` reads the table in the database of the tenant `acme` of the [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) instance `crm`. In a custom API called for a tenant, `instance: "crm"` reads it in the database of that tenant. **TypeScript** ```typescript await g.sys.system.getTableMeta({ instance: 'crm::acme', database: 'crm', collection: 'customers' }); ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-table-meta-api.md) --- # Create Indexes API > Create indexes on any database of the account with one system API of API Maker - the same request for MongoDB, MySQL, MariaDB, SQL Server, PostgreSQL and Oracle, safe to run again and again. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.html Creates one or more indexes with the same request on every database. An index which exists already is left alone, a field the database can not index is skipped with the reason, and the call can run again and again : it is what [Index Maker](https://docs.apimaker.dev/v4/extensions/index_maker/introduction.html) runs from its migration script `am_index_maker` on deploy. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/create-indexes` : `admin` is the user path of your account | | Body | `{ indexes: [ … ], ifNotExists?, continueOnError?, skipUnindexable? }` | | Answer | `data` : one result per definition, in order | | From code | [`g.sys.system.createIndexes`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.html#from-code) | ## The call **Body** ```json { "indexes": [ { "instance": "mysql8", "database": "inventory", "table": "orders", "fields": [ { "name": "customer_id" }, { "name": "status", "order": "DESC" } ] }, { "instance": "mongodb", "database": "inventory", "collection": "products", "name": "ux_products_sku", "unique": true, "fields": [ { "name": "sku" } ] } ], "ifNotExists": true, "continueOnError": true, "skipUnindexable": true } ``` | Property | Required | Meaning | |---|---|---| | `indexes` | yes | The definitions, one per index. | | `indexes[].instance` | yes | Instance name. `crm::acme` names the tenant `acme` of the multi-tenant instance `crm` ; in a call made for a tenant, the plain name is enough. | | `indexes[].database` | yes | Database name, as the account sees it. | | `indexes[].collection` or `table` | yes | The table. PostgreSQL : `public.orders`, SQL Server : `dbo.orders`. | | `indexes[].fields` | yes | `[{ name, order?, nullsFirst? }]`. `order` : `ASC` (default), `DESC`, `HASHED` (MongoDB), `TEXT` (MongoDB). `nullsFirst` : PostgreSQL. | | `indexes[].name` | no | Index name. Missing : `am_ix_
__` (`am_ixu_` when unique), cut to the limit of the database. | | `indexes[].unique` | no | A unique index. | | `indexes[].type` | no | MySQL : `FULLTEXT`, `SPATIAL` ; SQL Server : `CLUSTERED`, `XML`, `SPATIAL` ; PostgreSQL : `hash`, `gin`, `gist`, `spgist`, `brin`. | | `indexes[].method` | no | MySQL : `BTREE`, `HASH`, `RTREE`. | | `ifNotExists` | no, `true` | An index with the same name, or with the very same fields under another name, is `EXISTS` : nothing is created twice. | | `continueOnError` | no, `true` | An index the database refuses is `FAILED` in the results and the next ones go on. `false` : the call fails at the first refusal. | | `skipUnindexable` | no, `true` | A field of a type no plain index can hold (TEXT on MySQL, json on PostgreSQL, CLOB on Oracle, `varchar(max)` on SQL Server, a primary key…) is `SKIPPED` with the reason, before the database is asked. `false` : the database decides. | ## The answer ```json { "success": true, "statusCode": 200, "data": [ { "instance": "mysql8", "database": "inventory", "collection": "orders", "name": "am_ix_orders_customer_id_status_3f9a1c", "fields": [ "customer_id", "status" ], "status": "CREATED" }, { "instance": "mongodb", "database": "inventory", "collection": "products", "name": "ux_products_sku", "fields": [ "sku" ], "status": "EXISTS", "message": "The index \"ux_products_sku\" exists already." } ] } ``` | Status | Meaning | |---|---| | `CREATED` | The index was created, and logged in the index logs of Index Maker. | | `EXISTS` | An index with this name, or with the same fields, is there already. | | `SKIPPED` | A field can not go in a plain index of this database ; `message` says which and why. | | `FAILED` | The database refused ; `message` is its error. | ## Good to know - Every database is supported : MongoDB, MySQL, MariaDB, SQL Server, PostgreSQL and Oracle. The statements are the plain `CREATE INDEX` of each one, so they run on every version. - The API changes the database : the APIs Security Report lists it with the sensitive system APIs. Give it only to the groups that need it. - Every index created is logged with its source (`SYSTEM_API`, or `MIGRATION_SCRIPT` when the body says `"source": "MIGRATION_SCRIPT"`). ## From code `g.sys.system.createIndexes` makes the same call from your code, whatever the settings of this API say about HTTP callers. ### Create indexes The same request works on MongoDB, MySQL, MariaDB, SQL Server, PostgreSQL and Oracle ; an index which exists already is left alone (`EXISTS`), a field the database can not index is `SKIPPED` with the reason, a refusal of the database is `FAILED` with its message. **TypeScript** ```typescript const results = await g.sys.system.createIndexes({ indexes: [ { instance: 'mysql8', database: 'inventory', table: 'orders', fields: [{ name: 'customer_id' }, { name: 'status', order: 'DESC' }], }, { instance: 'mongodb', database: 'inventory', collection: 'products', name: 'ux_products_sku', unique: true, fields: [{ name: 'sku' }], }, ], ifNotExists: true, // default : nothing is created twice continueOnError: true, // default : one refused index does not stop the others skipUnindexable: true, // default : TEXT, json, CLOB... are skipped before the database is asked }); // results[0] : { status: 'CREATED', name: 'am_ix_orders_customer_id_status_3f9a1c', fields: ['customer_id', 'status'], ... } // results[1] : { status: 'EXISTS', message: 'The index "ux_products_sku" exists already.', ... } ``` ### An index of a tenant - Without a `name`, the index is called `am_ix_
__` (`am_ixu_` when unique), the same in every environment, cut to the limit of the database (30 characters on Oracle). - `order` : `ASC`, `DESC`, `HASHED` and `TEXT` (MongoDB) ; `nullsFirst: true` on PostgreSQL. `type` : `FULLTEXT` or `SPATIAL` (MySQL), `CLUSTERED`, `XML`, `SPATIAL` (SQL Server), `hash`, `gin`, `gist`, `spgist`, `brin` (PostgreSQL). `method` : `BTREE`, `HASH`, `RTREE` (MySQL). - This is the call the migration script `am_index_maker` of [Index Maker](https://docs.apimaker.dev/v4/extensions/index_maker/automatic_indexes.html) makes on deploy : a migration script of your own can do the same. - `instance: "crm::acme"` creates the index in the database of the tenant `acme` of the [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) instance `crm`. In a custom API called for a tenant, `instance: "crm"` creates it in the database of that tenant. **TypeScript** ```typescript await g.sys.system.createIndexes({ indexes: [{ instance: 'crm::acme', database: 'crm', collection: 'customers', fields: [{ name: 'email' }], unique: true }], }); ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.md) --- # Drop Indexes API > Drop indexes by name on any database of the account with one system API of API Maker - a missing index is reported, not an error, so the call can run again. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-drop-indexes-api.html Drops one or more indexes of a table by name, on any database, and reports what happened to each one. A name the table does not have is `NOT_FOUND`, not an error. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/drop-indexes` : `admin` is the user path of your account | | Body | `{ instance, database, collection | table, indexes: [ names ], ifExists?, continueOnError? }` or an array of them | | Answer | `data` : one result per name | | From code | [`g.sys.system.dropIndexes`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-drop-indexes-api.html#from-code) | ## The call **Body** ```json { "instance": "mysql8", "database": "inventory", "table": "orders", "indexes": [ "am_ix_orders_customer_id_status_3f9a1c", "old_idx_status" ], "ifExists": true, "continueOnError": true } ``` | Property | Required | Meaning | |---|---|---| | `instance` | yes | Instance name, `crm::acme` for a tenant of a multi-tenant instance. | | `database` | yes | Database name, as the account sees it. | | `collection` or `table` | yes | The table. | | `indexes` | yes | The names to drop. | | `ifExists` | no, `true` | A name the table does not have is `NOT_FOUND`. `false` : it is `FAILED`. | | `continueOnError` | no, `true` | A drop the database refuses is `FAILED` and the next ones go on. `false` : the call fails at the first one. | ## The answer ```json { "success": true, "statusCode": 200, "data": [ { "instance": "mysql8", "database": "inventory", "collection": "orders", "name": "am_ix_orders_customer_id_status_3f9a1c", "fields": [ "customer_id", "status" ], "status": "DROPPED" }, { "instance": "mysql8", "database": "inventory", "collection": "orders", "name": "old_idx_status", "fields": [], "status": "NOT_FOUND", "message": "The index \"old_idx_status\" does not exist." } ] } ``` - The `fields` of a dropped index are the ones it had. Every drop is logged in the index logs of Index Maker with its source. ## From code `g.sys.system.dropIndexes` makes the same call from your code, whatever the settings of this API say about HTTP callers. ### Drop indexes Indexes are dropped by name. A name the table does not have is `NOT_FOUND`, not an error, so the call can run again. **TypeScript** ```typescript const results = await g.sys.system.dropIndexes({ instance: 'mysql8', database: 'inventory', table: 'orders', indexes: ['am_ix_orders_customer_id_status_3f9a1c', 'old_idx_status'], ifExists: true, // default : a missing index is NOT_FOUND continueOnError: true, // default : one refused drop does not stop the others }); // results[0] : { status: 'DROPPED', fields: ['customer_id', 'status'], ... } // results[1] : { status: 'NOT_FOUND', message: 'The index "old_idx_status" does not exist.', ... } ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-drop-indexes-api.md) --- # Get Indexes API > List the indexes of a table on any database of the account in one common format with the get indexes system API of API Maker - name, key fields with their order, unique or not. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-indexes-api.html Lists the indexes of a table in the same format for every database : the name, the fields with their direction, whether it is unique, and the access method when the database says it. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/get-indexes` : `admin` is the user path of your account | | Body | `{ instance, database, collection | table }` or an array of them | | Answer | `data` : an array of indexes, or an array of arrays | | From code | [`g.sys.system.getIndexes`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-indexes-api.html#from-code) | ## The call **Body** ```json { "instance": "mysql8", "database": "inventory", "table": "orders" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "name": "PRIMARY", "key": { "id": 1 }, "unique": true }, { "name": "am_ix_orders_customer_id_status_3f9a1c", "key": { "customer_id": 1, "status": -1 }, "unique": false, "method": "BTREE" } ] } ``` | Property | Meaning | |---|---| | `name` | Index name. | | `key` | The fields in order, with their direction : `1` ascending, `-1` descending, `hashed` or `text` on MongoDB. | | `unique` | A unique index (the primary key included). | | `method` | The access method when the database tells it (`BTREE`, `HASH`, `gin`…). | - An array of tables answers an array of lists, in the same order. ## From code `g.sys.system.getIndexes` makes the same call from your code, whatever the settings of this API say about HTTP callers. ### The indexes of a table The indexes of a table, in the same format for every database. **TypeScript** ```typescript const indexes = await g.sys.system.getIndexes({ instance: 'mysql8', database: 'inventory', table: 'orders', }); // [ // { name: 'PRIMARY', key: { id: 1 }, unique: true }, // { name: 'am_ix_orders_customer_id_status_3f9a1c', key: { customer_id: 1, status: -1 }, unique: false, method: 'BTREE' }, // ] ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-indexes-api.md) --- # Emit Event API > Emit one or several events of API Maker over HTTP with the emit event system API - the event data reaches the listeners, and their outputs come back. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-api.html Emits an [event](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.html) of the account : its listeners run with the data, and the call answers what they returned. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/emit-event` : `admin` is the user path of your account | | Body | `{ name, eventData?, executeListeners? }` or an array of them | | Answer | `data` : the event with `outputArr`, one entry per listener | | From code | [`g.sys.system.emitEvent`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-api.html#from-code) | ## The call **Body** ```json { "name": "order-placed", "eventData": { "order_no": 1001 }, "executeListeners": [ "send-invoice" ] } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "name": "order-placed", "eventData": { "order_no": 1001 }, "executeListeners": [ "send-invoice" ], "outputArr": [ { "listenerName": "send-invoice", "output": { "notified": true } } ] } } ``` **Body : several events** ```json [ { "name": "order-placed", "eventData": { "order_no": 1001 } }, { "name": "stock-changed", "executeListeners": [ "sync-warehouse", "alert-low-stock" ] } ] ``` - `executeListeners` names the listeners to run ; absent, every listener of the event runs. - A chain which comes back to an event already running is stopped : no infinite loop. ## From code **TypeScript** ```typescript const result = await g.sys.system.emitEvent('order-placed', { order_no: 1001 }, [ 'send-invoice' ]); g.logger.log(result.outputArr); ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. The group must also grant the event. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-api.md) --- # Emit WebSocket Event API > Push a notification to the WebSocket clients subscribed to a custom event of API Maker with the emit event WS system API, from any server or any code. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-ws-api.html Sends the data to every client subscribed to a custom [WebSocket event](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html) whose subscription condition matches, whatever server the client is connected to. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/emit-event-ws` : `admin` is the user path of your account | | Body | `{ name, eventData? }` or an array of them | | Answer | `data` : `true` | | From code | [`g.sys.system.emitEventWS`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-ws-api.html#from-code) | ## The call **Body** ```json { "name": "default/orders-updated", "eventData": { "recipientId": "42", "order_no": 1001, "status": "SHIPPED" } } ``` **Body : several events** ```json [ { "name": "default/orders-updated", "eventData": { "recipientId": "42", "order_no": 1001 } }, { "name": "stock-alerts", "eventData": { "sku": "WM-1", "qty": 2 } } ] ``` - The event must exist on the WebSocket Events page. A client receives the data when its subscription has no condition, or when the emitted data has the values of its `criteria`. - To notify one person, put their id in the data and let their client subscribe with that id in `criteria`. ## From code **TypeScript** ```typescript await g.sys.system.emitEventWS('default/orders-updated', { recipientId: '42', order_no: 1001 }); ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. The group must also grant the WebSocket event. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key, `WebSocket event not found with name …` for an unknown event. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-ws-api.md) --- # Is Valid Data for Table API > Check data against the schema of a table without saving it with the is valid data for table system API of API Maker - the same conversions and validations as a save or an update, the errors as the answer. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-table-api.html Runs the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of a table on some data without writing anything, and answers the list of errors a save (or an update) would have refused it with. Empty list : the data is fine. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/is-valid-data-for-table` : `admin` is the user path of your account | | Body | `{ instance, database, collection | table, data, sendingTo? }` or an array of them | | Answer | `data` : an array of errors (empty when valid), or an array of arrays | | From code | [`g.sys.system.isValidDataForTable`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-table-api.html#from-code) | ## The call **Body** ```json { "instance": "mongodb", "database": "shop", "collection": "products", "data": { "name": "M", "currency": "EUR", "price_cents": -5, "contact_email": "x@example.com" } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "type": "schemaKeyNotFound", "field": "contact_email", "message": "Property not found in schema for key 'contact_email'.", "code": 400 }, { "type": "minLength", "field": "name", "message": "Property 'name' should have minimum length of '2'.", "code": 400 }, { "type": "enumValidation", "field": "currency", "message": "Property 'currency' should have any value from [IN, US, GB].", "code": 400 }, { "type": "min", "field": "price_cents", "message": "Please provide minimum '0' for 'price_cents' field.", "code": 400 } ] } ``` | Key | Meaning | |---|---| | `instance`, `database`, `collection` or `table` | The table whose schema is used. A table without a schema is refused. | | `data` | One object, or an array of objects : then the answer is an array of error lists, one per object. | | `sendingTo` | `SAVE` (default) : `required` fields must be there. `UPDATE` : only the fields present are checked, the way update by id checks them. | - The call is `success: true` even when the data is not : the errors are the answer, not a failure of the call. - An array of bodies checks several tables in one call. ## From code **TypeScript** ```typescript const errors = await g.sys.system.isValidDataForTable({ instance: 'mongodb', database: 'shop', collection: 'products', data: g.req.body, sendingTo: 'SAVE' }); if (errors.length) { g.res.statusCode = T.EStatusCode.BAD_REQUEST; return errors; } ``` - Useful for a wizard which validates each step before the final save, or for an import which reports every bad row before writing any. ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key, `Unable to find schema for …` for a table without a schema. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-table-api.md) --- # Is Valid Data for Custom API > Check a body or query params against the request schema of a custom API without calling it, with the is valid data for custom API system API of API Maker. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-custom-api.html Runs the `reqBodySchema` or `reqQueryParametersSchema` of a [custom API](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html) on some data and answers the errors the API would refuse it with, without running the API. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/is-valid-data-for-custom-api` : `admin` is the user path of your account | | Body | `{ name, type, data }` or an array of them | | Answer | `data` : an array of errors (empty when valid), or an array of arrays | | From code | [`g.sys.system.isValidDataForCustomAPI`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-custom-api.html#from-code) | ## The custom API settings **Basic info of the custom API** ```typescript let customApi: T.ICustomApiSettingsTypes = { name: 'Create Customer', path: '/customers', requestMethod: T.ERequestMethod.POST, errorList: [], reqBodySchema: { first_name: { __type: T.EType.string, validations: { required: true } }, phone: { __type: T.EType.number, validations: { required: true, min: 5, max: 999 } }, }, }; ``` ## The call **Body** ```json { "name": "Create Customer", "type": "BODY", "data": { "phone": 3 } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "type": "required", "field": "first_name", "message": "Please provide valid 'first_name' field with type 'string'.", "code": 400 }, { "type": "min", "field": "phone", "message": "Please provide minimum '5' for 'phone' field.", "code": 400 } ] } ``` | Key | Meaning | |---|---| | `name` | The name of the custom API. | | `type` | `BODY` for `reqBodySchema`, `QUERY_PARAMS` for `reqQueryParametersSchema`. | | `data` | The body or the query params to check. | ## From code **TypeScript** ```typescript const draft = g.req.body; const errors = await g.sys.system.isValidDataForCustomAPI({ name: 'Create Customer', type: T.ECustomAPIDataValidationType.BODY, data: draft }); ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key, `Unable to find custom API with name …`. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-custom-api.md) --- # Multi Tenant Instance Updated API > Drop the connection pools of a tenant on every API Maker server after its connection string changed, with the multi tenant instance updated system API, so its next request uses the new database. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-multi-tenant-instance-updated-api.html Call it when the connection string of a tenant of a [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) instance changes in the tenants table : every server drops the connection pools of that tenant and the cached answers of the instance and of the tenants table, so the next request of the tenant reads its row again and connects to the new database. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/multi-tenant-instance-updated` : `admin` is the user path of your account | | Body | `{ instanceName, username }` or an array of them | | Answer | `data` : one answer per item, `{ success, data }` | | From code | [`g.sys.system.multiTenantInstanceUpdated`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-multi-tenant-instance-updated-api.html#from-code) | ## The call **Body : one tenant of one instance** ```json { "instanceName": "crm", "username": "acme" } ``` **Body : several** ```json [ { "instanceName": "crm", "username": "acme" }, { "instanceName": "billing", "username": "acme" } ] ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "success": true, "data": true }, { "success": false, "data": false, "error": { "code": 400, "message": "Instance 'billing' is not a multi tenant structure instance." } } ] } ``` - Each item is answered on its own : an unknown instance (404) or an instance which is not multi-tenant (400) does not stop the other items. - Both fields are required. - Until this API is called, the servers keep using the pools they have. For PostgreSQL, the pools of every database of the tenant are dropped. ## From code **TypeScript** ```typescript await g.sys.system.multiTenantInstanceUpdated({ instanceName: 'crm', username: 'acme' }); ``` ### Several instances at once Several instances or tenants at once, each item answered on its own: **TypeScript** ```typescript const output = await g.sys.system.multiTenantInstanceUpdated([ { instanceName: 'crm', username: 'acme' }, { instanceName: 'billing', username: 'acme' }, ]); // [{ success: true, data: true }, { success: false, data: false, error: { code: 404, message: "..." } }] ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-multi-tenant-instance-updated-api.md) --- # Is Valid Connection String API > Test a database connection string before saving it with the is valid connection string system API of API Maker - MongoDB, MySQL, MariaDB, SQL Server, PostgreSQL and Oracle - the check behind the Test Instance button. Source: https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-connection-string-api.html Opens a connection with the string given and closes it : the check the **Test Instance** button of the instances page makes. Useful for an onboarding screen which lets a customer register their own database, or before writing a tenant row. | | | |---|---| | Method | POST | | URL | `/api/system-api/admin/is-valid-connection-string` : `admin` is the user path of your account | | Body | `{ connectionString, instanceType, oracleDBUsername?, oracleDBPassword?, oracleDBPrivilege? }` or an array of them | | Answer | `data` : `{ success, data: true | false, error? }`, or an array of them | | From code | [`g.sys.system.isValidConnectionString`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-connection-string-api.html#from-code) | ## The call **Body** ```json { "connectionString": "postgresql://crm:secret@db-1:5432/crm_acme", "instanceType": "POSTGRE_SQL_DB" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "success": true, "data": true } } ``` **Answer when the database refuses** ```json { "success": true, "statusCode": 200, "data": { "success": false, "data": false, "error": "password authentication failed for user \"crm\"" } } ``` | Key | Meaning | |---|---| | `connectionString` | The string, in the format of the database : see the [connection strings](https://docs.apimaker.dev/v4/docs/Database-connection-string/mongodb-connection-strings.html) pages. | | `instanceType` | `MONGO_DB`, `MYSQL_DB`, `MARIA_DB`, `SQL_SERVER_DB`, `POSTGRE_SQL_DB` or `ORACLE_DB`. | | `oracleDBUsername`, `oracleDBPassword`, `oracleDBPrivilege` | Oracle : the user, the password and the privilege, as on the instance form. | - An array of bodies checks several strings in one call and answers an array in the same order. ## From code **TypeScript** ```typescript const cs = g.req.body.connectionString; const check = await g.sys.system.isValidConnectionString({ connectionString: cs, instanceType: T.EInstanceType.POSTGRE_SQL_DB }); if (!check.success) throw new Error('This database can not be reached : ' + check.error); ``` ## Access and settings - Over HTTP, a system API answers once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) give it `apiAccessType: TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grants this system API, in `x-am-authorization`) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : your code calls it through `g.sys`, the admin panel tests it, and an HTTP call is refused. - The settings can also cache the answer or require person tokens (`authProviders`) ; [pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) run around it like around any API. - The [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html) apply : `x-am-response-case`, `x-am-content-type-response`, `x-am-internationalization`, `x-am-tenant-username`… ## Errors | Code | When | |---|---| | `400` | The body is wrong : the message names the missing or invalid key. | | `401` | No valid API user token, or the API is `NO_ACCESS` : `You are not authorized to access this API.` | | `403` | No group grants this system API. | ## Related - [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) · [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-connection-string-api.md) --- # Events and Listeners > Events in API Maker - named events with any number of listeners in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin, emitted from your code, from the emit-event system API or automatically after an API hit, with the outputs of the listeners returned and infinite chains stopped. Source: https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.html An event is a name with listeners : functions which run when the event is emitted, each in the [language](https://docs.apimaker.dev/v4/docs/languages/index.html) it was written in. Your code emits it, a client emits it through the system API, or API Maker emits it on its own after an API you attach it to. Events keep the side effects of an operation (mails, stock, notifications, exports) out of the API which triggers them. > Diagram : Events : emitted by code, by the system API or after an API hit, with N listeners, and no infinite loops > > Events in API Maker : an event is emitted by your code with emitEvent, by the emit-event system API, or automatically after an API it is attached to ; its listeners, your TypeScript functions, run in the sandbox ; a listener can emit another event, and a chain which comes back to an event already running is stopped. ## Create an event `APIs & Logic → Events`, then **+**. | Field | Meaning | |---|---| | Name | The name used to emit it. Unique in the account. | | Automatically trigger on API hit | On : the event is emitted after every successful call of the APIs listed under it. | | Trigger on APIs | Instance APIs (an API of a table), custom APIs, system APIs. Several at once. | | Listeners | One or more, each with a name, a code and its language, a timeout in minutes, versions, and `runOnNativeProcess`. | | Save response in log | Keep what the listeners returned in the log table. | ## A listener **TypeScript** ```typescript import * as T from 'types'; import * as db from 'db-interfaces'; async function main(g: T.IAMGlobal) { const data = g.req.eventData; // what was emitted await g.sys.db.saveSingleOrMultiple({ instance: 'mongodb', database: 'shop', collection: 'notifications', saveData: { order_no: data.order_no, text: `Order ${data.order_no} placed` }, }); return { notified: true }; // returned to the emitter } module.exports = main; ``` - Every listener gets the same `g` as a custom API : `g.req.eventData` holds the emitted data, `g.sys` reaches every API, `g.logger` writes to the log table. - Listeners run one after the other, each within its timeout : in the sandbox and with `runOnNativeProcess` alike. Empty : `sandboxReqTimeout` of the configuration (13 seconds), whatever time the request which emitted the event has. - For an **automatic trigger**, `g.req.eventData` is the whole answer of the API which triggered it (`{ success, statusCode, data, … }`), so a listener sees what was saved or read. ## Emit from your code **TypeScript** ```typescript const result = await g.sys.system.emitEvent('order-placed', { order_no: 1001, customer_id: 42 }); // result.outputArr : [ { listenerName: 'send-invoice', output: { notified: true } }, … ] await g.sys.system.emitEvent('order-placed', { order_no: 1001 }, [ 'send-invoice' ]); // only these listeners ``` - The third argument names the listeners to run ; absent, every listener runs. - The emitter gets the output of each listener back, so an event can also be a way to run several pieces of code and collect their results. ## Emit over HTTP ```text POST /api/system-api/admin/emit-event ``` ```json { "name": "order-placed", "eventData": { "order_no": 1001 }, "executeListeners": [ "send-invoice" ] } ``` - An array of such objects emits several events in one call. See [Emit event](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-api.html). ## Chains without loops A listener may emit another event, which may emit another. API Maker keeps the list of the events already running for the request : an emit which comes back to one of them is stopped, so a chain never becomes an infinite loop. Each level of depth is allowed ; only the cycle is refused. ## Where it fits | Need | Use | |---|---| | Something must happen after an API, in code | An event with an automatic trigger, or a [post hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) when it must change the answer | | A client must be told live | A [WebSocket event](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html), emitted with `emitEventWS` | | Something must happen on a schedule | A [scheduler](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.html) | ## Good to know - Events and their listeners are files in Git and deploy with a pull. Listeners have versions. - Test a listener from the [API testing page](https://docs.apimaker.dev/v4/docs/features/developer-tools.html#api-testing-page) : pick the event, the listener, put the event data in the body and send. - On the Events page, **Move up** and **Move down** order the listeners, **Grid View** lists every listener of every event, **Clone** copies an event with its listeners and **Copy full event name** gives the name to emit. - On the native process, `console.log` is not captured : use `g.logger`. - A [log profile](https://docs.apimaker.dev/v4/docs/logs/logs-and-dashboards.html#log-profiles) records each listener run. - Groups grant events : an API user needs a group with the event to emit it through the system API. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.md) --- # WebSocket Events > Push live updates to web and mobile apps with API Maker - connect a WebSocket with the tokens, subscribe to the answers of tables, custom and system APIs or to your own events with a condition, and emit from your code with emitEventWS. Source: https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html API Maker runs a WebSocket server on the port of its APIs (`38246` on a server set up by the install script, `wss://` behind Caddy). A client connects with its tokens, subscribes to what it wants to hear about, and receives a message every time a matching API succeeds on any server of the cluster, or when your code emits a custom event. No polling, no message broker of your own. > Diagram : WebSockets : connect with the tokens, register with a condition, get notified from any server > > WebSocket notifications in API Maker : a browser or mobile app opens a WebSocket on the port of the APIs (38246) with its tokens in the query string, receives a CONNECTED frame, sends a REGISTER frame naming the APIs, tables or custom events it wants with a condition, and gets NOTIFICATION frames when a matching API succeeds on any server of the cluster or when code emits the event ; Redis carries the notifications between servers. ## 1. Connect The tokens go in the query string of the WebSocket URL, with the same names as the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html). ```javascript const ws = new WebSocket( 'wss://ws.example.com/' // the WebSocket URL of your API Maker + '?x-am-authorization=' + encodeURIComponent(apiUserToken) + '&x-am-user-authorization=' + encodeURIComponent(personToken) // when your events need the person ); ``` - `x-am-authorization` : the API user, always. The person token of the [auth provider](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) of the events you subscribe to : `x-am-user-authorization`, `x-google-authorization`, `x-azure-authorization`, `x-aws-authorization` or `x-custom-authorization`. API Maker finds out which provider a token belongs to ; `&authProviders=name1,name2` narrows it when you want. - The first message from the server is `CONNECTED` : ```json { "type": "CONNECTED", "response": { "connected": true } } ``` - A bad token closes the connection with a `TOKEN_VALIDATION` message which says which header is wrong, then the close code `1000` with the reason `Invalid client`. ## 2. Subscribe Send a `REGISTER` message with the events you want, then read the answer : the ones accepted, with their `eventId`, and the ones refused with the reason. ```javascript ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.type === 'CONNECTED') { ws.send(JSON.stringify({ objType: 'REGISTER', onEvents: [ { // every successful get all on this table eventType: 'INSTANCES', apiName: 'SCHEMA_GET_ALL', instance: 'mongodb', database: 'shop', collection: 'orders', getEventData: true, select: { order_no: 1, status: 1 }, }, { // a custom event, only when the criteria matches the emitted data eventType: 'CUSTOM_WS_EVENTS', apiName: 'default/orders-updated', condition: { conditionType: 'RESPONSE', criteria: { recipientId: myUserId } }, getEventData: true, }, ], })); } else if (msg.type === 'REGISTER') { console.log(msg.response.validOnEvents, msg.response.invalidOnEvents); // eventId per subscription } else if (msg.type === 'NOTIFICATION') { render(msg.response.eventId, msg.response.eventData); } }; ``` | Key of an event | Meaning | |---|---| | `eventType` | `INSTANCES`, `CUSTOM_APIS`, `SYSTEM_APIS` or `CUSTOM_WS_EVENTS`. | | `apiName` | The [API id](https://docs.apimaker.dev/v4/docs/apis-all/overview.html#api-ids) for a table (`SCHEMA_GET_ALL`, `GEN_POST_BULK_INSERT`…), the name of the custom API, the id of the system API (`ENCRYPT_DATA`…), or the name of the custom WebSocket event. | | `instance`, `database`, `collection` or `table` | The table, for `INSTANCES`. | | `condition` | `{ conditionType: 'RESPONSE', criteria: { field: value } }` : only the answers whose data has these values reach this client. For a custom event, the emitted data must match the criteria exactly, which is how one notification reaches one person. | | `select` | The fields of the data to send, `{ field: 1 }`. | | `getEventData` | `true` sends the data ; `false` sends only the fact that it happened. | - Subscriptions are checked against the groups of the API user and the auth provider of the event : `invalidOnEvents` says why one was refused. ## 3. Receive ```json { "type": "NOTIFICATION", "response": { "eventId": "…", "eventData": [ { "order_no": 1001, "status": "PAID" } ] } } ``` - `eventId` is the one of the register answer : it says which subscription the notification is for. `eventData` is there when the subscription asked for it with `getEventData`. - Notifications come from **any server** of the cluster : Redis passes them between the servers. - Unsubscribe with the `eventId` of the register answer : ```javascript ws.send(JSON.stringify({ objType: 'UNREGISTER', onEvents: [ eventId ] })); ``` ## When a notification is sent - After every **successful** call of a subscribed API : the answer of the call is the data, filtered by `condition` and `select`. The call can come over HTTP or from your code (custom APIs, hooks, events…). - Not when an error happens before the answer exists : a pre hook which throws sends nothing. A post hook which throws does not stop the notification. - For a custom WebSocket event, when your code emits it : **TypeScript** ```typescript await g.sys.system.emitEventWS('default/orders-updated', { recipientId: '42', type: 'order-updated', order_no: 1001 }); ``` ```text POST /api/system-api/admin/emit-event-ws { "name": "default/orders-updated", "eventData": { "recipientId": "42", "order_no": 1001 } } ``` - To notify N people, emit once per person with their id in the data ; each client subscribes with its id in `criteria`. ## Custom WebSocket events `APIs & Logic → WebSockets` lists the event names of the account. A custom event needs to exist there before it can be subscribed to or emitted. | Field | Meaning | |---|---| | Name | The `apiName` of the subscription and the name given to `emitEventWS`. | | Auth providers | The provider whose token a client must have validated to subscribe. Empty : the first auth provider of the account. | | Can user connect code | A function which decides whether a client may subscribe : return `{ canConnect: true }` or `{ canConnect: false, errorText: '…' }`. It sees the tokens in `g.req.auth`. | | Trigger on API | Optional : an API whose success also emits this event. | ## Who may subscribe A subscription is taken when all of these hold, and `invalidOnEvents` says which one did not : - **The groups of the API user grant the event** : every WebSocket event, or the ones the [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) names. That goes for the events of tables, of custom and system APIs, and for the custom WebSocket events of the account alike. - **The client connected with a token of an auth provider of the event**, when the account has auth providers. - **The can user connect code of the event says yes**, when the event has one. What a client proved when it connected is looked at again while it stays connected : | What changes | What the client sees | |---|---| | The API user is deactivated or removed, its password changes, or the account is deactivated. | A `TOKEN_VALIDATION` message which says why, then the close code `1000` (`Invalid client`). Its subscriptions are gone. | | A token the client connected with runs out : the token of the API user, or its person token. | A `TOKEN_VALIDATION` message with the code `401` which names the token, then the close code `1000` (`Invalid client`). Its subscriptions are gone. See [Staying connected](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#staying-connected). | | The root user changes the secret the tokens of API users are signed with (root user settings). | A `TOKEN_VALIDATION` message with the code `401` which names the token, then the close code `1000` (`Invalid client`), for every client which connected with a token of the secret before. Its subscriptions are gone. See [Staying connected](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#staying-connected). | | A group stops granting an event, or the API user leaves the group. | `{ "type": "UNREGISTER", "response": { "eventId": "…" } }` for each subscription which is taken back. The connection and the other subscriptions stay. | | A group starts granting an event. | Nothing : the client can subscribe to it from then on, without connecting again. | | A custom WebSocket event is removed. | `{ "type": "UNREGISTER", "response": { "eventId": "…" } }` for its subscriptions. | - A connection lasts as long as the first of its tokens : until the `exp` of the token of the API user and of the person token it connected with. A Google token counts five minutes longer, as its check does ; a token checked by the code of a custom provider counts until the `exp` that code answers, when it answers one. A token without an end closes nothing. ## The connection - **Ping** : the server pings every client once a minute. A WebSocket client answers by itself (browsers and the usual libraries do, there is nothing to write), which keeps proxies from closing a quiet connection. A client may stay silent for hours ; one which answers nothing at all for two and a half minutes is taken for gone and closed. - **Close codes** : `1000` `Invalid client` when the tokens do not let the client in, or do not any more (one of them ran out, the API user changed) ; `1001` `Server is going away` when the server stops or restarts, which is no refusal. - **Reconnect** : subscriptions belong to the connection. When it closes, connect again and send `REGISTER` again ; after `1001` another server of the cluster takes the client at once. See [Staying connected](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#staying-connected). - **Size** : a message of a client is at most 16 MiB (a `REGISTER` is a few hundred bytes). A longer one ends the connection. - **A client which does not read** : what it is told waits for it up to 64 MiB, and is skipped past that until it reads again. The connection stays. - **Many clients** : a client which waits costs a little memory and almost no CPU. How many a server holds, and what to look at before it holds them, is in [Many clients](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#many-clients). ## Staying connected A client stays connected for as long as it is reachable and its tokens are valid. It sends nothing for that : no request to repeat, no heartbeat of its own. The answers to the pings of the server are sent by the browser or the WebSocket library. | What closes a connection | What the client sees | What it does | |---|---|---| | One of its tokens runs out. | A `TOKEN_VALIDATION` message with the code `401` which names the token, then the close code `1000`. | Gets a new token (with the [refresh token](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html), or a new login), connects again and sends `REGISTER` again. | | The API user or the account is deactivated, or the password of the API user changes. | A `TOKEN_VALIDATION` message which says why, then the close code `1000`. | The same, once it has a token which is valid again. | | The root user changes the secret the tokens of API users are signed with. | A `TOKEN_VALIDATION` message with the code `401` which names the token, then the close code `1000`. | Gets a new token, which is signed with the new secret, connects again and sends `REGISTER` again. A token of the secret before, and its refresh token, are not accepted any more : a new login. | | The server stops or restarts. | The close code `1001` (`Server is going away`). | Connects again with the same tokens : another server of the cluster takes it at once. | | Its machine or its network is gone : nothing came from it for two and a half minutes, not even the answers to the pings. | Nothing : it is not there. | Connects again when it is back. | **A token ran out** ```json { "type": "TOKEN_VALIDATION", "errors": [ { "code": 401, "message": "The token of 'x-am-authorization' expired : connect again with a new token." } ] } ``` **The secret of the tokens changed** ```json { "type": "TOKEN_VALIDATION", "errors": [ { "code": 401, "message": "The secret the token of 'x-am-authorization' is signed with changed : connect again with a new token." } ] } ``` One handler covers all five : when the socket closes, come back with a token which is valid at that moment, and subscribe again. ```javascript function connect() { getToken().then((token) => { // yours : a token which is valid now, renewed when it has to be const ws = new WebSocket('wss://ws.example.com/?x-am-authorization=' + encodeURIComponent(token)); ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.type === 'CONNECTED') ws.send(JSON.stringify({ objType: 'REGISTER', onEvents: myEvents })); else if (msg.type === 'NOTIFICATION') render(msg.response.eventId, msg.response.eventData); }; // Whatever closed it : come back, and subscribe again. Not all at the same moment : a second, and up to four more. ws.onclose = () => setTimeout(connect, 1000 + Math.random() * 4000); }); } connect(); ``` - **Wait a random moment before connecting again** : when a server stops, every client it held sees the close at the same instant. Clients which all come back after exactly one second arrive together ; with a random wait they arrive spread over a few seconds, and each one is let in sooner. See [Many clients](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#many-clients). - **How long a token lasts** : `expires_in` of the [token answer](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html), 72 hours unless `expiresInSeconds` or the configuration say otherwise. The tokens of Google, Azure AD and AWS Cognito usually last an hour. - **Nothing is sent again** : what happens between the close and the new `REGISTER` does not reach the client afterwards. A client which may miss nothing opens a second connection with its new token a little before the old token runs out, sends `REGISTER` there, and closes the first one once the second answered. ## Many clients A client which waits costs memory, and almost nothing else : its socket, its tokens and its subscriptions. How many clients a server holds is a question of its memory, not of its CPU. | Server | Clients connected at once | Memory per client | New clients a second | A notification, p50 | |---|--:|--:|--:|--:| | [1 shared vCPU · 1 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-shared-1-vcpu-1gb.html) | **26,484** | 8.6 KB | 2,602 | 2.51 ms | | [2 shared vCPUs · 4 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-shared-2-vcpu-4gb.html) | **338,016** | 8.5 KB | 4,041 | 1.45 ms | | [4 shared vCPUs · 8 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-shared-4-vcpu-8gb.html) | **801,972** | 8.7 KB | 7,009 | 1.06 ms | | [8 shared vCPUs · 32 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-shared-8-vcpu-32gb.html) | **3,662,564** | 8.8 KB | 10,939 | 0.94 ms | | [2 dedicated vCPUs · 4 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-dedicated-2-vcpu-4gb.html) | **338,006** | 8.5 KB | 3,464 | 1.74 ms | | [4 dedicated vCPUs · 8 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-dedicated-4-vcpu-8gb.html) | **795,888** | 8.7 KB | 6,627 | 1.04 ms | | [8 dedicated vCPUs · 16 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-dedicated-8-vcpu-16gb.html) | **1,732,035** | 8.8 KB | 10,093 | 0.83 ms | Measured on 14 October 2026 with API Maker 4.0.0 on Linode servers set up by the install script, MongoDB and Redis on the same server. Every client connected with both tokens and one subscription ; « new clients a second » connect, subscribe and leave ; the notification is for one person, from the HTTP call which emits it to the client. As API servers which share one Redis : 2,022,026 clients on 1 × 8 vCPU · 16 GB, 1,915,960 clients on 2 × 4 vCPU · 8 GB, 1,619,372 clients on 4 × 2 vCPU · 4 GB. Every run, with its memory second by second : [WebSocket benchmarks](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/index.html). - **Count the servers from the memory** : the clients one server holds, and one server more as a spare. Every server of a cluster takes clients, and all of them share the Redis of API Maker, which carries each notification to the server which holds the client : a load balancer needs no sticky session. [Scale to N users](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/scaling.html) has the picture and the rule. - **Open files** : every connected client is one open file of the server. The install script lets the service open as many as the firewall of the machine tracks connections : 256 for each MB of memory, 1,048,576 at the least (`LimitNOFILE` : about 4 million on a server of 16 GB) ; started any other way, API Maker raises its own limit as far as the system allows, and says in its log how many connections it takes. The WebSockets never take all of them : a sixteenth of the connections (64 at the least, 4,096 at the most) is kept for the calls of your APIs, which would else find no connection while the clients stay. A client which comes when the server holds as many WebSockets as it takes is answered `503` with `Retry-After: 5` : it comes back, as after any close. - **The connection table of the firewall** : a Linux server which runs Docker tracks every connection (`nf_conntrack`), and a connection which closed stays in the table for a while. With its default size (262,144 connections) the table can fill before the memory does when many clients come and go, and the kernel then drops new connections without a word to anybody (`nf_conntrack: table full, dropping packet` in `dmesg`). The install script sizes the table from the memory of the server (`/etc/sysctl.d/90-api-maker.conf`), and API Maker warns in its log at start when the table is smaller than the clients the server could hold. - **Clients which all come back together** : after a restart, or when a server of a cluster stops, the clients it held connect again within the same seconds. API Maker lets 256 of them in at a time (the check of their tokens), handles the messages of 256 clients of an account at a time, and accepts no more than 1,024 connections ahead of the ones it is serving : the others wait for their turn as sockets, their request unread in the kernel, about 4 KB each. That is memory on top of what the clients take once they are in : a server with a quarter of its memory free has its clients back in seconds or minutes, one full to its last 5 % lets them back a few hundred at a time (8 GB and 800,000 clients) : the [benchmarks](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/methodology.html#restart) restart a server with three quarters of the clients it holds. A client which waits a random moment before it connects again helps too (see [Staying connected](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#staying-connected)). - **Clients which all go together** : when a network or a load balancer goes, every client of a server leaves in the same second. Their sockets are closed at once, and Redis is told behind them. - **A server which is killed** : a server which stops takes its clients out of Redis first. One which is killed, or whose machine goes, cannot. So every server says every ten seconds that it is alive, and once the word of one of them ran out (45 seconds), another server takes out of Redis what it left, or the same one does when it comes back. Within a minute the calls of your APIs do not find its clients any more, and do not count them among the clients they told. The clients themselves connect again, as after any close. - **Calls which come faster than the clients are told** : an API answers before its clients are told, and a call takes far less than telling a thousand clients. Past 16,384 notifications which wait to be told, for each thread of the server, the next call whose answer is told to clients waits before it is answered, ten seconds at the most. Callers are slowed down to the pace their clients are told at : the memory of the server stays level, and a notification is not late by all the work which piled up before it. - **The memory of the network** : Linux gives all its TCP connections together a part of the memory of the machine (`net.ipv4.tcp_mem`, about 9 % as it sets it up). A connection which holds a short message nobody read yet, or one its client did not acknowledge yet, holds a page of it : nothing for a few thousand clients, all of it when hundreds of thousands of them do something in the same second, and Linux then slows every connection of the machine down. API Maker looks at its open connections once a second and raises that memory as they come, two pages for each connection : only upwards, never above a quarter of the memory of the machine ([`tcpMemoryPercent`](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html), 0 to leave the kernel alone), and not at all on a server with fewer connections than the kernel has room for. It says so in its log ; when it may not write the numbers (it is not root, or runs in a container), it says the `sysctl` line to run by hand. - **Leave room** : a server which is full of clients has no memory left for the work of its APIs. Keep a quarter of the memory free : the clients of a server which stops then fit on the others. ## From your code - `g.sys.system.emitEventWS(name, data)` from any custom API, hook, event, scheduler or migration. - The sample custom API `/default/ws-notify` of a new account has the complete client snippet in its comments. - The [system API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-ws-api.html) does the same over HTTP. ## Good to know - The admin panel and the local client use the same server with their own tokens : what they ask and what they are told (the files of the account) is for them only, an API user is refused it. - Groups grant WebSocket events : an API user subscribes to a custom event only when a group allows it. - The WebSocket is on the port of the APIs : at the address of `BE_HOST_PORT`, with `ws://` for `http://` and `wss://` for `https://`. Behind Caddy, it is on the domain of the APIs. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.md) --- # Schedulers > Run code on a schedule in API Maker, in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin - intervals from every second to every year or a cron expression, a time zone per interval, one run per cluster spread over the servers with failover, a timeout, versions, and start, stop or run now from the admin panel. Source: https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.html A scheduler is a function which runs on a schedule, written in [any language](https://docs.apimaker.dev/v4/docs/languages/index.html) of API Maker, : every few seconds, every night at two, on the first of the month, or on any cron expression, in the time zone you pick. On a cluster of servers each run happens once, on one server, and moves to another one when a server goes down. > Diagram : Schedulers on a cluster : every interval runs once, on one server, spread over the servers, with failover > > Schedulers on a cluster of three API Maker servers : each scheduler has intervals with a time zone ; at each tick exactly one server of the cluster runs it, the schedulers are spread over the servers, and when a server goes down another one picks its schedulers up. ## Create a scheduler `APIs & Logic → Schedulers`, then **Add New**. | Field | Meaning | |---|---| | Name | Unique in the account. It names the folder in Git ; a rename is followed by the log profiles. | | Timeout in minutes | The run is stopped after this long, in the sandbox and on the native process alike. Empty : 10 minutes. | | Code | The function, with the global object `g`. | | Intervals | One or more, each with a time interval, a time zone and an active switch. | | Active | The master switch of the scheduler. | | Run Native | Run in API Maker itself instead of the sandbox. | ## The code **TypeScript** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const before = Date.now() - 7 * 86400000; // a week ago, in milliseconds since 1970 : what updated_at holds const removed = await g.sys.db.removeByQuery({ instance: 'mongodb', database: 'shop', collection: 'carts', find: { updated_at: { $lt: before } }, }); g.logger.log(`${removed.deletedRowsCount} carts removed`); return { removed: removed.deletedRowsCount }; } module.exports = main; ``` - The same `g` as a custom API, without a request : `g.req.body` and `g.req.query` are empty. Keep state between runs in the [cache](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.html#from-code) : the next run may be on another server. - Pick the language in the title bar of the editor. ## Intervals | Group | Choices | |---|---| | Seconds | every second, 5, 10, 15, 30 seconds | | Minutes | every minute, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30 minutes | | Hours | every hour, 2, 3, 4, 6, 8, 12 hours | | Days | every day, 2, 3, 5, 10, 15 days | | Working days | Monday to Friday at 11:30 | | Months | every month, 2, 3, 4, 6 months | | Year | every year | | Custom | a cron expression | - Each interval has its **time zone** : `Asia/Kolkata`, `Europe/Paris`, `UTC`… A scheduler with two intervals runs both. - Each interval has its own active switch, so one can be paused without touching the other. ### Custom cron expressions Six fields, seconds first : `second minute hour day-of-month month day-of-week`. Five fields work too (the seconds default to `0`). | Field | Values | |---|---| | second, minute | 0-59 | | hour | 0-23 | | day of the month | 1-31 | | month | 1-12, or `jan`-`dec` | | day of the week | 0-7 (0 and 7 are Sunday), or `sun`-`sat` | Each field takes `*`, a value, a range `1-5`, a list `1,15` and a step `*/10`. The presets `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`, `@minutely`, `@secondly`, `@weekdays` and `@weekends` stand for a whole expression. The panel checks an expression as you type it, by the same rules. | Expression | Runs | |---|---| | `0 0 22 * * 1-5` | at 22:00, Monday to Friday | | `0 15 14 1 * *` | at 14:15 on the first of every month | | `*/30 * * * * *` | every 30 seconds | | `0 0 */6 * * *` | every 6 hours | ## On a cluster - **Once per tick.** Whatever the number of API Maker servers, an interval runs once : each scheduler belongs to one server, which holds it in Redis, and a scheduler already running is not started again. - **Spread out.** Every server knows the servers which are up (each one says so every few seconds), and the schedulers are spread over them evenly. A server which joins takes its share; the others keep theirs. - **At once.** A server starts its schedulers as soon as it starts. - **Failover.** When a server stops cleanly, it gives its schedulers back and the others take them at their next look, within five seconds ; when it dies, within about twenty seconds, when its hold runs out. - **Which server.** The **Server** column of the Schedulers page shows the server running each scheduler. - **Safe deployments.** A Git pull or a deployment does not interrupt a run in progress ; the next run uses the new code. ## Start, stop, run now - The **Active** switch starts and stops a scheduler right away, on every server : stop a misbehaving job in production in one click. - **Run now** from the [API testing page](https://docs.apimaker.dev/v4/docs/features/developer-tools.html#api-testing-page) (pick the scheduler, send) runs it once, outside its schedule, and shows the result and the logs. - The Schedulers page marks a scheduler running now on a node, and one whose intervals are all switched off : it never runs. **Clone** copies a scheduler. - Versions : keep several versions of the code, one active. ## Good to know - A [log profile](https://docs.apimaker.dev/v4/docs/logs/logs-and-dashboards.html#log-profiles) records every run with its duration, and what it returned when **Log Response** is ticked for the scheduler. - Schedulers are files in Git (`src/schedulers//`) and deploy with a pull. Their active state and intervals travel with them. - A scheduler which needs more than its timeout should split the work : page through the rows and keep a cursor in the cache. - Groups grant schedulers to API users for the testing page and the run API. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.md) --- # The Global Object g > Everything your code gets in API Maker through g - the request (headers, params, query, body, auth, event data), the response (status, content type, output, headers, errors), g.sys for the database, system and cache APIs, the logger and the shared space. Source: https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html Every piece of code you write in API Maker, a custom API, a hook, an event listener, a scheduler, a migration, a process initializer, a test case, is a function of one argument, `g`. In TypeScript : **TypeScript** ```typescript import * as T from 'types'; import * as db from 'db-interfaces'; async function main(g: T.IAMGlobal) { const rows = await g.sys.db.getAll({ instance: 'mongodb', database: 'shop', collection: 'products' }); g.logger.log(rows.length, 'products'); return rows; } module.exports = main; ``` | | | |---|---| | `g.req` | What came in : headers, params, query, body, event data, the opened tokens. | | `g.res` | What goes out : status code, content type, output, headers, errors, warnings. | | `g.sys` | The APIs of API Maker from code : `db`, `db.gen`, `system`, `cache`, and `test` inside a test case. | | `g.logger` | `debug`, `log`, `info`, `warn`, `error` : shown with the response on the testing pages, kept by the [log profiles](https://docs.apimaker.dev/v4/docs/logs/logs-and-dashboards.html#log-profiles). | | `g.shared` | Values every request of the same runner shares, for as long as it runs : a cache, a counter. | ## In every language Custom APIs, hooks, schedulers and event listeners can also be written in [Python, PHP, C#, Go, Rust, Java and Kotlin](https://docs.apimaker.dev/v4/docs/languages/index.html). They get the same `g`, in the words of their language : | | Request | Answer | Database | Logger | Shared | Refuse | |---|---|---|---|---|---|---| | TypeScript | `g.req.body.id` | `g.res.statusCode = 201` | `await g.sys.db.getAll({…})` | `g.logger.log('a')` | `g.shared.n` | `throw [{ code: 400, message }]` | | Python | `g.req.body.id` | `g.res.status_code = 201` | `await g.sys.db.get_all('mysql_8', 'inventory', 'customers', limit=10)` | `g.logger.log('a')` | `g.shared.get('n')` | `raise g.Errors(message, code=400)` | | PHP | `$g->req->body->id` | `$g->res->statusCode = 201` | `$g->sys->db->getAll('mysql_8', 'inventory', 'customers', limit: 10)` | `$g->logger->log('a')` | `$g->shared->n` | `throw new AmErrors($message, 400)` | | C# | `g.Req.Body["id"]` | `g.Res.StatusCode = 201` | `await g.Sys.Db.GetAll(new GetAll("mysql_8", "inventory", "customers") { Limit = 10 })` | `g.Logger.Log("a")` | `g.Shared.GetValueOrDefault("n")` | `throw new AmErrors(message, 400)` | | Go | `g.Req.Body().Get("id")` | `g.Res.SetStatusCode(201)` | `g.Sys.Db.GetAll(am.GetAll{Instance: "mysql_8", Database: "inventory", Collection: "customers", Limit: 10})` | `g.Logger.Log("a")` | `g.Shared.Get("n")` | `return nil, am.Fail(400, message)` | | Rust | `g.req.body()["id"]` | `g.res.set_status_code(201)` | `g.sys.db.get_all(GetAll::new("mysql_8", "inventory", "customers").limit(10))?` | `g.logger.log("a")` | `g.shared.get("n")` | `Err(am::fail(400, message))` | | Java | `g.req.getBody().get("id")` | `g.res.setStatusCode(201)` | `g.sys.db.getAll(new GetAll("mysql_8", "inventory", "customers").limit(10))` | `g.logger.log("a")` | `g.shared.get("n")` | `throw new AmErrors(message, 400)` | | Kotlin | `g.req.body["id"]` | `g.res.statusCode = 201` | `g.sys.db.getAll(GetAll("mysql_8", "inventory", "customers").limit(10))` | `g.logger.log("a")` | `g.shared["n"]` | `throw AmErrors(message, 400)` | Each language page has the full reference and examples : [TypeScript](https://docs.apimaker.dev/v4/docs/languages/typescript.html) · [Python](https://docs.apimaker.dev/v4/docs/languages/python.html) · [PHP](https://docs.apimaker.dev/v4/docs/languages/php.html) · [C#](https://docs.apimaker.dev/v4/docs/languages/csharp.html) · [Go](https://docs.apimaker.dev/v4/docs/languages/go.html) · [Rust](https://docs.apimaker.dev/v4/docs/languages/rust.html) · [Java](https://docs.apimaker.dev/v4/docs/languages/java.html) · [Kotlin](https://docs.apimaker.dev/v4/docs/languages/kotlin.html). The code editor completes `g` in each of them, and offers the columns of your tables on the rows a call answers : [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor). ## g.req | Key | Holds | |---|---| | `headers` | The request headers, editable : a pre hook can set `x-am-content-type-response` for the whole call. | | `params` | The path parameters : `instanceName`, `database`, `collection`, `id`, `primaryKey`, `field` and `order` of distinct, `tenantUsername` for a [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) request… | | `query` | The query parameters as an object : `find`, `select`, `limit`… editable in a pre hook, which is how [row level security](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html) is done. | | `body` | The body, editable. | | `eventData` | The data of the event, in an event listener. | | `auth` | The opened tokens : `authAMUser` (the API user), `authAMDB` (the person, from a DB auth provider), `authCustom`, `authGoogle`, `authAWS`, `authAzure`. | | `reqInfo` | `url`, `apiCategory`, `reqMethod` and `apiInfo` (`id`, `name`, `schemaType`) : which API is running. | | `isApiRequestFromUser` | `true` for an HTTP request, `false` for a call made from code. | **TypeScript** **Who is calling** ```typescript const person = g.req.auth.authAMDB; // the row of your users table, minus its password const apiUser = g.req.auth.authAMUser; // name and groups of the API user ``` ## g.res | Key | Holds | |---|---| | `statusCode` | `T.EStatusCode.OK` (200), `CREATED`, `NO_CONTENT`, `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `RESOURCE_NOT_FOUND`, `INTERNAL_SERVER_ERROR`… Set by your code, it wins over the one of the API. | | `contentType` | `T.EContentType.JSON` (default), `XML`, `YAML`, `TEXT`, `HTML`, `OCTET_STREAM`. | | `output` | The answer. In a post hook, the answer of the API, editable. A custom API returns its output instead. | | `headers` | Headers to add to the reply. | | `errors`, `warnings` | The lists of the reply. | | `shared` | Values of this request : set in a pre hook, read in the API or its post hook. | **TypeScript** **An HTML page from a custom API** ```typescript g.res.contentType = T.EContentType.HTML; return '

Hello

'; ``` - To send a file, return an object with `__am__downloadFilePath` or `__am__downloadFileOrFolderPaths` : see [files](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html#files). ## g.sys | Part | Holds | |---|---| | `g.sys.db` | The [schema APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) of any table : `getAll`, `getById`, `query`, `saveSingleOrMultiple`, `masterSave`, `updateById`, `updateMany`, `replaceById`, `removeById`, `removeByQuery`, `aggregate`, `count`, `distinct`, `distinctQuery`, `arrayOperations`, `getAllByStream`, `queryByStream`. [Examples](https://docs.apimaker.dev/v4/examples/sys/db/getAll.html). | | `g.sys.db.gen` | The same calls on the generated APIs, without schema : `getAllGen`, `queryGen`… [Examples](https://docs.apimaker.dev/v4/examples/sys/db/getAll.html#without-a-schema). | | `g.sys.system` | The [system APIs](https://docs.apimaker.dev/v4/examples/sys/system/system.html) : `encrypt`, `decrypt`, `hash`, `getToken`, `getSecret`, `callExternalApi`, `executeQuery`, `getTableMeta`, `createIndexes`, `dropIndexes`, `getIndexes`, `emitEvent`, `emitEventWS`, `isValidDataForTable`, `isValidDataForCustomAPI`, `multiTenantInstanceUpdated`, `isValidConnectionString`. | | `g.sys.cache` | Your own keys in Redis and the cache resets : `getKey`, `setKey`, `removeKey`, `resetCacheDB`, `resetCacheCustomApis`, `resetCacheSystemApis`. [Examples](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.html#from-code). | | `g.sys.test` | In a [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html) only : `runCustomApi`, `mock`, `clearMocks`, `calls`. | - Every `g.sys.db` call takes the same `headers` and `queryParams` an HTTP call takes, and runs with the groups of the API user of the request. Hooks are skipped unless you pass `skipHookRunning: false`. - Pass `true` as the last argument (`getFullResponse`) in TypeScript, or call through `full` in the other languages (`g.sys.db.full.get_all` in Python, `g.Sys.Db.Full.GetAll` in C# and Go), to get the whole `{ success, data, errors, … }` envelope instead of `data`. ## g.logger and g.shared **TypeScript** ```typescript const order = { _id: 'A-1001' }; g.logger.log('order ', order._id); // with the response on the testing pages, in the logs of a log profile g.logger.error('payment failed : ', 'card declined'); g.res.shared.count = 234; // set in a pre hook… const count = g.res.shared.count; // …read in the post hook of the same request g.shared.visits = (g.shared.visits || 0) + 1; // kept by the runner for the requests which follow ``` - `debug`, `log`, `info`, `warn` and `error` write the same line : the level is not kept. Objects are printed as indented JSON. In TypeScript the arguments are joined without a separator, in the other languages with a space. The calls are synchronous, no `await`. - For an error, log its message (`e.message` in TypeScript) or its stack. - On the native process (`runOnNativeProcess`), `console.log` is not captured : use `g.logger`. - `g.shared` belongs to one runner : a sandbox has several runners, and a server several sandboxes, so a value there is a cache, never the only copy of something. Values for the rest of one request go in `g.res.shared`. ## Related - [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) · [Code examples](https://docs.apimaker.dev/v4/examples/index.html) · [Cheat sheet](https://docs.apimaker.dev/v4/docs/cheat-sheet.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.md) --- # Utility Classes > Share code between all your code in API Maker with utility classes - written in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin, imported the way each language imports, in custom APIs, hooks, events, schedulers, migrations and test cases, with interfaces, versions and Git. Source: https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html A utility class is code shared by all your code : helpers, validations, pricing rules, clients of external services, interfaces. Write it once on the **Utility Classes** page (`APIs & Logic → Utility Classes`), in TypeScript or in [any other language](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html#other-languages) API Maker runs, and use it in all the code of that language. In TypeScript you import it anywhere with `utils/`. ## Create one Click **+**, give a path (a folder to group them) and a name. The name is unique, starts with a capital letter, and is what you import. **TypeScript** **The utility class Calculator** ```typescript class Calculator { add(x: number, y: number) { return x + y; } multi(x: number, y: number) { return x * y; } } let temp = new Calculator(); export = temp; ``` **A custom API which uses it** ```typescript import * as T from 'types'; import * as calc from 'utils/Calculator'; async function main(g: T.IAMGlobal) { return { sum: calc.add(3, 6), product: calc.multi(3, 6) }; } module.exports = main; ``` **A custom API which uses it** ```python import Calculator as calc async def main(g): return {'sum': calc.add(3, 6), 'product': calc.multi(3, 6)} ``` **A custom API which uses it** ```php Calculator::add(3, 6), 'product' => Calculator::multi(3, 6)]; } ``` **A custom API which uses it** ```csharp file static class Api { public static async Task Main(G g) { return new { sum = Calculator.Add(3, 6), product = Calculator.Multi(3, 6) }; } } ``` **A custom API which uses it** ```go package code import ( "apimaker/am" calc "apimaker/src/utility_classes/utils---Calculator" ) func Main(g *am.G) (any, error) { return am.M{"sum": calc.Add(3, 6), "product": calc.Multi(3, 6)}, nil } ``` **A custom API which uses it** ```rust use am::{Errors, G, Value, json}; use crate::Calculator as calc; pub fn main(g: &mut G) -> Result { Ok(json!({ "sum": calc::add(3.0, 6.0), "product": calc::multi(3.0, 6.0) })) } ``` **A custom API which uses it** ```java import am.G; import java.util.Map; class Api { static Object main(G g) { return Map.of("sum", Calculator.add(3, 6), "product", Calculator.multi(3, 6)); } } ``` **A custom API which uses it** ```kotlin import am.G fun main(g: G): Any? { return mapOf("sum" to Calculator.add(3.0, 6.0), "product" to Calculator.multi(3.0, 6.0)) } ``` - In TypeScript the file exports an **instance** (`export = temp`), so the importer calls the methods directly. The other languages name what a class declares the way they import things : see [below](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html#other-languages). - The class can hold state : a connection opened in a method stays for the life of the sandbox. ## Use one - Works in custom APIs, pre and post hooks, event listeners, schedulers, migration scripts, process initializers, WebSocket connect code and test cases. - Press Ctrl+Space after `utils/` in the editor to see every class. - A method which needs API Maker takes `g` as a parameter : `Pricing.total(g, order)`. ## The classes of a language The list of the page has a dropdown next to its search : **All languages**, then each language on each of its runtimes (TypeScript on Node.js, TypeScript on Bun, Python, C# on .NET…), with how many classes each one shows. A choice shows the classes the code of that language uses, in the tree and in the grid : the classes written in TypeScript or JavaScript for Node.js and for Bun alike, the classes written in Python for Python. A class added while the list shows a language starts in that language. On the API testing page, **Show Utility Classes** next to the code opens the dialog of the classes with the ones the code of the editor uses : those of its language, on its runtime. Pick **All languages**, or another language, in the dialog to see the others. An API without code of its own opens it with every class. ## Interfaces A utility class can export interfaces and types, for every piece of code to share (a class, a record, a struct in the other languages) : **TypeScript** **The utility class Inf** ```typescript class Inf { } let temp = new Inf(); export = temp; export interface IUserP { firstName: string; lastName: string; phone: number; } ``` **A custom API which uses it** ```typescript import * as T from 'types'; import * as inf from 'utils/Inf'; async function main(g: T.IAMGlobal) { const data: inf.IUserP = { firstName: 'John', lastName: 'Doe', phone: 123456789 }; return data; } module.exports = main; ``` **A custom API which uses it** ```python from dataclasses import asdict from Inf import UserP async def main(g): data = UserP(firstName='John', lastName='Doe', phone=123456789) return asdict(data) ``` **A custom API which uses it** ```php Main(G g) { return new UserP("John", "Doe", 123456789); } } ``` **A custom API which uses it** ```go package code import ( "apimaker/am" inf "apimaker/src/utility_classes/utils---Inf" ) func Main(g *am.G) (any, error) { return inf.UserP{FirstName: "John", LastName: "Doe", Phone: 123456789}, nil } ``` **A custom API which uses it** ```rust use am::{Errors, G, Value}; use crate::Inf::UserP; pub fn main(g: &mut G) -> Result { let data = UserP { first_name: "John".to_owned(), last_name: "Doe".to_owned(), phone: 123456789 }; Ok(serde_json::to_value(data)?) } ``` **A custom API which uses it** ```java import am.G; class Api { static Object main(G g) { return new Inf.UserP("John", "Doe", 123456789); } } ``` **A custom API which uses it** ```kotlin import am.G fun main(g: G): Any? { return UserP("John", "Doe", 123456789) } ``` ## In every language Pick the language of the class in the title bar of its editor. A new class starts as a small example of the language, with a comment which says how the other code names it. Each language does it its own way, the same in a sandbox and in the Git repository : | Language | The class `Tax` in the folder `utils/` | In the code | |---|---|---| | TypeScript | the module `utils/Tax` | `import * as Tax from 'utils/Tax'`, `Tax.add(100)` | | Python | the module `Tax` | `from Tax import add` | | PHP | what its file declares (no `namespace` of its own) | `Tax::add(100)`, `add_tax(100)`, `TAX_RATE` | | C# | what its file declares, compiled with the code | `Tax.Add(100)` | | Go | a package, imported by its folder | `import tax "apimaker/src/utility_classes/utils---Tax"` | | Rust | the module `Tax` of the crate of the code | `use crate::Tax::add;` | | Java | the class `Tax`, compiled with the other classes of Java | `Tax.add(100)` | | Kotlin | what its file declares (it starts as an `object Tax`), compiled with the other classes of Kotlin | `Tax.add(100.0)` | **TypeScript** **The utility class Tax** ```typescript export const RATE = 0.2; export function add(amount: number): number { return Math.round(amount * (1 + RATE) * 100) / 100; } ``` **A custom API** ```typescript import * as T from 'types'; import * as Tax from 'utils/Tax'; async function main(g: T.IAMGlobal) { return { total: Tax.add(g.req.body.amount) }; } module.exports = main; ``` **A custom API** ```python from Tax import add async def main(g): return {"total": add(g.req.body.amount)} ``` **A custom API** ```php Tax::add($g->req->body->amount)]; } ``` **A custom API** ```csharp file static class Api { public static async Task Main(G g) { return new { total = Tax.Add(g.Req.Body.GetDouble("amount")) }; } } ``` **A custom API** ```go package code import ( "apimaker/am" tax "apimaker/src/utility_classes/utils---Tax" ) func Main(g *am.G) (any, error) { return am.M{"total": tax.Add(g.Req.Body().Float("amount"))}, nil } ``` **A custom API** ```rust use am::{Errors, G, Value, json}; use crate::Tax; pub fn main(g: &mut G) -> Result { Ok(json!({ "total": Tax::add(g.req.body()["amount"].as_f64().unwrap_or(0.0)) })) } ``` **A custom API** ```java import am.G; import java.util.Map; class Api { static Object main(G g) { return Map.of("total", Tax.add(g.req.getBody().getDouble("amount"))); } } ``` **A custom API** ```kotlin import am.G fun main(g: G): Any? = mapOf("total" to Tax.add(g.req.body.getDouble("amount"))) ``` - A class is used by the code of its own language; code of another language reaches what it does through a custom API. - A class which is saved is what the next request runs with : nothing restarts. - The rules of names, what a class keeps between requests, and how the repository resolves the same imports are on [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html#utility-classes). - Declarations for the editors (`utils/` and Ctrl+Space) and interfaces are for the classes written in TypeScript. The coverage of test runs counts the lines of the classes of every language. ## Versions A utility class has versions and one active version. The code which imports it always gets the active one, at once, without a restart. Keep the old version until the new one has proven itself, and switch back in one click. ## Testing A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#testing-a-utility-class) imports the class the way custom APIs do and calls its methods, with mocks for the `g.sys` calls it makes and coverage of its lines. ## Good to know - Utility classes are files in Git under `src/utility_classes/` and deploy with a pull. - A utility class runs where its importer runs : in the sandbox, or on the native process for a native custom API. - A new account comes with a sample utility class to read. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/utility-class/utility-class.md) --- # Process Initializers > Code which runs when a sandbox process of your account starts, before any API - subscribe to MQTT or Redis, warm a cache, prepare a client - with the same g as a custom API. Source: https://docs.apimaker.dev/v4/docs/features/process-initializers.html A process initializer runs when a sandbox process of your account starts, before it serves any of your code. It is the place for work which must exist in every process : a subscription to a message broker, a client kept for the life of the process, a warm cache. | | | |---|---| | Page | `APIs & Logic → Initializers` : name, order, timeout in minutes, active, and the code. | | Runs | In every sandbox process, when it starts ; the active initializers in the order given, one after the other. | | Code | The function `main` of any [language](https://docs.apimaker.dev/v4/docs/languages/index.html), with the same `g` as a custom API. | | Test | **Test API** runs it on the API testing page. | **TypeScript** **Keep the rates in the memory of the process** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { // Read once per process : every request this process serves finds them in g.shared. const rates = await g.sys.db.getAll({ instance: 'mysql_8', database: 'shop', collection: 'rates' }); g.shared.rates = rates; g.logger.log(`${rates.length} rates loaded`); } module.exports = main; ``` - An initializer is also where what lives as long as the process starts : the client of a message queue which listens to a topic (MQTT, Kafka, RabbitMQ…), with the package of its language added on the [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages) page. - Other code runs on an action : an HTTP call, a schedule, an event. An initializer runs on its own, once per process, so keep it light and idempotent. - The number of sandbox processes is set on the [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) page : each one runs the initializers. - `Timeout in minutes` is how long each process waits for the initializer, every time it runs it (empty : `sandboxReqTimeout` of the configuration, 13 seconds). A process whose initializer did not end in time goes on to the next one, and then serves your code : keep what lasts as long as the process (a listener, a subscription) out of the promise the initializer returns. ## Related - [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) · [Schedulers](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/features/process-initializers.md) --- # Test Cases Feature > Write, run and debug test cases for custom APIs and utility classes in API Maker - in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin - with mocks, a run from the terminal (npm run test) and the code coverage of every run, in every language, in VS Code and WebStorm. Source: https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html - In API Maker, you can write test cases of your custom APIs and utility classes. - They run on the server, in the sandbox of your account, with the same `g` your custom APIs get. You can run them from the **Test Cases** page (menu `APIs & Logic → Test Cases`), or from the folder of the local client with `npm run test`. - A test case is written in TypeScript or in [any other language](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) API Maker runs : Python, PHP, C#, Go, Rust, Java or Kotlin. The custom APIs it tests can be written in any language too, the same one or another. - Every run reports which lines of your custom APIs, utility classes and test cases ran (code coverage), in every language, on the page and in your IDE. ## Quick start 1. **Add a test case** `APIs & Logic → Test Cases` → add : a name, the type (custom API or utility class) and the item it tests. Saving gives you its default code. 2. **Write the tests** Call the custom API with `g.sys.test.runCustomApi` (`run_custom_api` in Python…) and check what it answers. Give [mocks](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#mocks) to the calls of `g.sys` which must not reach the database or the web. 3. **Run them** `Ctrl+Enter` on the page, or `npm run test` in the folder of the local client : the results, then the lines which ran. 4. **Run them on every push** The same command in a [CI job](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#in-a-ci-job), its exit code `0` when everything passed. ## Run from the terminal : `npm run test` The git repository of the account holds the local client (`localClient.js`), and its `package.json` has two scripts, committed with the repository : `npm run start` keeps your files in sync with the server, `npm run test` runs the test cases. Neither needs `npm install`. ``` npm run test # every test case of the account, as they are in this folder npm run test -- orders # only the test cases whose name, custom API or utility class contains "orders" npm run test -- --no-sync # run what the server has, without sending the changes of this folder npm run test -- --push # this folder wins where it differs from the server (a CI job) npm run test -- --no-coverage # without the code coverage npm run test -- --bail # stop after the first test case file which fails npm run test -- --json run.json # also write the whole result to a file npm run test -- --help # every option ``` - **The code of your folder runs.** `npm run test` first merges the folder with the server, as `npm run start` does when it starts : what you changed in the folder (a test case, the custom API it tests, a utility class) is sent to the server, what changed on the server is written to the folder, and the run waits until the server saved what was sent. Then the test cases run on the server, in the sandbox of the account, with their mocks, and every step is printed as it happens. - When `npm run start` runs in the same folder, it keeps the folder in sync already : `npm run test` starts the run at once. `--no-sync` runs what the server has, without merging. - The first merge of a folder which never synced (a fresh clone) takes the version of the server for every file which differs, and keeps the local one in `.local-client/backups`. With `--push` the folder wins instead : every item which differs from the server is sent to it (what only the server has is written to the folder, nothing is deleted on the server). An item the server can not save (code which does not compile) is said, and the run goes on with what the server has for it. - The exit code is `0` when everything passed, `1` when a test failed, `2` when the run did not complete, so it fits in a script or a CI job. - `Ctrl+C` cancels the run on the server. ### In a CI job `local-client.config.js` holds the sync token and is not in git. A CI job gives the settings as variables of its environment instead : `AM_WEB_SOCKET_URL`, `AM_SYNC_TOKEN` (a secret of the job) and `AM_ADMIN_USER_PATH`. A variable wins over the file when both are there. **.github/workflows/test-cases.yml** ```yaml name: test cases on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm run test -- --push --json test-run.json env: AM_WEB_SOCKET_URL: wss://api.example.com AM_SYNC_TOKEN: ${{ secrets.AM_SYNC_TOKEN }} AM_ADMIN_USER_PATH: admin ``` - A CI job clones the repository afresh : `--push` sends the code of the commit to the server, so the test cases of the commit run against the code of the commit. Point the job at a server which may take the code of the branch : a server for development or for tests, never production. - `coverage/lcov.info` and the file of `--json` are there after the run, for a coverage service or an artifact of the job. ## Code coverage in your IDE Every run collects which lines ran. `npm run test` writes them to `coverage/lcov.info` (and `coverage/coverage-summary.json`) in the folder of the local client, in the standard lcov format, with the absolute paths of the files of the repository on your machine (`/src/custom_apis/orderCount/orderCount.ts`, `.../src/utility_classes/...`, `.../src/test_cases/...` ; a file of another language by its own extension, `orderCount.py`), the way istanbul writes them. - **VS Code** : install the extension **Coverage Gutters**. Run `Coverage Gutters: Watch` (or `Display Coverage`) from the command palette : the lines which ran are marked green in the gutter, the ones which did not red, in the files of `src`. The marks refresh after every `npm run test`. - **WebStorm** : `Run → Show Coverage Data…`, then `+` and pick `coverage/lcov.info`. The coverage shows in the gutter of the editor and in the project tree. - The `coverage` folder is in the `.gitignore` of the repository : it is made on every run, for the IDE, never for git. - The coverage counts the custom APIs run with `g.sys.test.runCustomApi`, the utility classes the tests use, and the test cases themselves. A custom API called through the web (`g.sys.system.callExternalApi`) runs in a request of its own and is not part of it. - Every language counts its own lines : TypeScript through the profiler of V8 (Node.js), Python with `sys.monitoring`, Go with `go tool cover`, PHP, C# and Rust with a counter the runner writes before each statement, on the line of the statement, and Java and Kotlin with counters written into the compiled classes, at each line the compiler noted, all in a build of the code for the test run only. A line is a line of the code as it was written; the code which serves the API is never touched. - The utility classes are counted the same way, in their own language, and are in the report with the files of their classes in the repository (`src/utility_classes/utils---Tax/Tax.py`). A class the tests did not use is not in the report. - A statement which is the only one of an `if` or a loop without braces is counted with the `if` or the loop, not on its own (PHP, C#). A member of C# written as an expression (`public static double Add(double amount) => ...;`) counts its line. - A sandbox running on Bun has no profiler : the run says so, and reports no coverage of TypeScript. - Java and Kotlin count their lines on JDK 24 and later : on an older JDK (`21`) the tests run as well, and the coverage has no lines of theirs. - The file of a custom API in the report is its file in git : `src/custom_apis/shippingCost/shippingCost.ts`, or `.py`, `.php`, `.cs`, `.go`, `.rs`, `.java`, `.kt` for code of another language. ## The Test Cases page The page has three parts : - **Left** : every test case of the account, with the state its last run gave it (passed, failed, not run). Search by name, custom API or utility class, filter by state, use the arrow keys to move and `Enter` to run. The play button next to a test case runs that one. - **Middle** : the code of the selected test case. `Ctrl+S` saves, `Ctrl+Enter` runs it. After a run, the line of every failure is marked, and the gutter shows which lines ran (coverage). The **Mocks** button opens the mocks of the test case (see below). The picker in the title bar chooses the **language** of the test case and the **runtime** which runs it : a test case nobody wrote in yet becomes the default test case of the language you pick. - **Right** : the run going on, or the last one : each file and each test as the server reports them, the failures with what was expected and what came, then the coverage of everything which ran. A run started from a terminal (`npm run test`) or from another tab shows here as well. The switch **Collect the code coverage** at the bottom of the list decides whether the runs of this browser collect the coverage. It slows a run a little. ## Add a new test case ### Basic info - Give the name of the test case. - Select the type of the test case : Custom API or Utility class. - Select the custom API or the utility class it is about ('Test Item'). ### Mocks - The **Mocks** tab holds the mocks of the test case : calls of `g.sys` (`g.sys.db.getAll`, `g.sys.system.hash`, `g.sys.cache.getKey` ...) which answer with your data instead of reaching the database, redis or the web. When the tab is empty, it offers mocks to start from : rows of a table, one row by its id, a count, an external API, a secret, a cached value, a call which fails. - Each mock reads as a sentence : **when the test calls** a method, **with the arguments** (optional, one condition per argument, in order), **answer with** a value or **fail with an error**, **for** a number of calls (optional). - Write values the way a test writes them : JSON (`{ "collection": "orders" }`, `[1, 2]`, `"text"`, `42`, `true`, `null`), objects written the JavaScript way (`{ collection: 'orders' }`) or plain text. The type comes from what you write. - An object matches an argument which has the same keys with the same values, inside objects too : `{ "collection": "orders", "find": { "status": "paid" } }` matches a query of the orders whose `find` has `status: 'paid'`, whatever else the query and its `find` have. A list matches a list of as many items. A key whose value is `null` matches `null` or no such key. A text, a number or a boolean must be equal. Without a condition, the mock answers every call of the method. An empty answer answers `null`. - **Fail with an error** : the call fails as the API would, and the code sees its errors. Write the message (`The database is down.`), an error (`{ "message": "The database is down.", "code": 503 }`) or a list of errors ; an error without a `code` has 500. A call which asks for the whole answer (`getFullResponse`) gets `{ success: false, statusCode, errors }` instead. Use it to test what your code does when the database, the cache or another server fails. - **For** a number of calls : the mock answers the first calls it matches, then the mocks below it (or the real API) answer. A first call which fails and a second one which works, to test a retry. - The mocks are tried from the top : the first one whose method and arguments match answers. The tab warns about a mock which an earlier one hides, and about a condition on an argument the call never receives. - **In code** shows the same mock written for the code of a test, ready to copy. - Mocks can also be written in the code, next to the test which needs them (see below). They come first, so they win over the mocks of the tab. ### Description - Here you can add more readable text to understand the test case. Once you save the new test case you get its default code, in the language picked in the title bar : **TypeScript** ```typescript import * as T from 'types'; import * as db from 'db-interfaces'; import * as assert from 'node:assert'; module.exports = [{ name: 'Test case name 1', code: async (g: T.IAMGlobal) => { assert.strictEqual(10, 10); } }]; ``` - In TypeScript a test case exports a list of tests, one object per test with its name and its code ; the other languages name their tests the way their own test tools do (see [the table below](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages)). - Every test gets the same `g` as a custom API : `g.sys` for the APIs of API Maker, `g.logger` for logs which show with the result, `g.req` and `g.res`. ## Mocks in the code Next to a test, `mocks` holds the mocks of that test alone (in TypeScript) ; the other languages add them with `g.sys.test.mocks(…)` at the start of the test. A mock answers with a value, fails the call with an error, or answers with a function of the arguments of the call, as many times as it says, in every language : **TypeScript** ```typescript import * as T from 'types'; import * as assert from 'node:assert'; const ORDERS = { instance: 'mysql_8', database: 'shop', collection: 'orders' }; module.exports = [{ name: 'mocks answer values, errors and functions', mocks: [ // a value, when the query has these keys (inner objects too) { method: 'db.query', when: [{ collection: 'orders', find: { status: 'paid' } }], return: [{ id: 1, total: 20 }, { id: 2, total: 30 }] }, // an error, for the first call only : the next call gets the next mock { method: 'db.count', error: { message: 'The database is busy.', code: 503 }, times: 1 }, { method: 'db.count', return: 2 }, // a function of the arguments of the call (it may be async) { method: 'db.getById', return: (query: any) => ({ id: query.id, total: query.id * 10 }) }, ], code: async (g: T.IAMGlobal) => { const rows = await g.sys.db.query({ ...ORDERS, find: { status: 'paid', customer: 7 } }); assert.strictEqual(rows.length, 2); await assert.rejects(g.sys.db.count(ORDERS), (errors: any) => errors[0].code === 503); assert.strictEqual(await g.sys.db.count(ORDERS), 2); const order: any = await g.sys.db.getById({ ...ORDERS, id: 4 }); assert.strictEqual(order.total, 40); assert.strictEqual(g.sys.test.calls('db.count').length, 2); // the calls the mocks answered }, }]; ``` A mock is made of : | Key | What it is | |---|---| | `method` | The call : the id of the API (`SCHEMA_GET_ALL`) or its name in code (`g.sys.db.getAll`, `db.getAll`). The methods of the Mocks tab are the ones which can be mocked : every call of `g.sys.db`, `g.sys.db.gen`, `g.sys.system` and `g.sys.cache`. | | `when` | The condition of the first argument, or a list of conditions, one per argument, in order. A value (an object matches an argument which has what it has, inside objects too), or a function of the argument which says whether the mock answers. No `when` : every call of the method. | | `return` | The answer : a value, or a function of the arguments of the call. What the function returns is the answer, what it throws (raises, returns as an error) is the failure of the call. | | `error` | The call fails with these errors instead : a message, an error `{ message, code }`, a list of errors, or `true`. An error without a `code` has 500. | | `times` | The mock answers this many calls, then the next mock (or the real API) answers. No `times` : every call. | A function is given the way each language gives one : | Language | An answer | A condition | |---|---|---| | TypeScript | `return: (query) => …`, `async` too | `when: [(query) => …]` | | Python | `returns=lambda query: …`, or a function, `async def` too | `when=lambda query: …` | | PHP | `returns: fn($query) => …` | `when: fn($query) => …` | | C# | `Return = Mock.Answer(args => …)`, `Mock.AnswerAsync(async args => …)` | `When = Mock.Match(query => …)` | | Go | `Return: func(query am.M) (any, error) { … }`, or an `am.MockAnswer` of the arguments | `When: func(query am.M) bool { … }` | | Rust | `.answers(\|args\| Ok(…))` | `.when_matches(\|query\| …)` | | Java | `.answers(args -> …)` | `.whenMatches(query -> …)` | | Kotlin | `.answers { args -> … }` | `.whenMatches { query -> … }` | - The function runs in the test, in its language, also when the call comes from a custom API the test runs in another language : the API asks the test. - `g.sys.test.mock(...)` adds a mock while the test runs, `g.sys.test.clearMocks(method?)` removes mocks, `g.sys.test.calls(method?)` gives the calls the mocks answered so far in this test. - A call which asks for the whole answer (`getFullResponse`, `g.sys.db.full…`) gets what the mock answers as its `data`, or `{ success: false, statusCode, errors }` from a mock which fails it. - A call which no mock answers reaches the real API, with the full permissions of the account. ## Testing a custom API `g.sys.test.runCustomApi` runs the code of a custom API of the account inside the test, with the request you describe. The `g.sys` calls the API makes are answered by the mocks of the test, and the coverage of the run sees the API. The hooks of the API are not run : it is the code of the API alone. **TypeScript** ```typescript import * as T from 'types'; import * as assert from 'node:assert'; module.exports = [{ name: 'orderCount answers the number of orders', mocks: [{ method: 'db.getAll', when: [{ collection: 'orders' }], return: [{ id: 1 }, { id: 2 }, { id: 3 }] }], code: async (g: T.IAMGlobal) => { const out = await g.sys.test.runCustomApi({ name: 'orderCount', query: { customer: 7 }, headers: { 'x-role': 'admin' } }); assert.deepStrictEqual(out, { count: 3 }); // the whole answer : data, logs, headers, warnings const full = await g.sys.test.runCustomApi({ name: 'orderCount', query: { customer: 7 } }, true); assert.deepStrictEqual(full.logs, ['3 orders']); // what the API throws is thrown here await assert.rejects(g.sys.test.runCustomApi({ name: 'orderCount', body: { fail: true } }), /failed on purpose/); }, }]; ``` - `name` is the name of the custom API, `version` (its version or the name of the version) runs another version than the one the API serves. - `body`, `query`, `params` and `headers` are the request the API sees. ### A custom API in another language A custom API written in Python, PHP, C#, Go, Rust, Java or Kotlin is tested the same way : `g.sys.test.runCustomApi` runs it in the sandbox of its language (its group of separate sandboxes when it has one), and each of its calls of `g.sys` asks the test first whether one of its mocks answers it. Mocks which are functions run in the test, in the language of the test; what they return reaches the API in its language. **The custom API 'shippingCost', in Python** ```python async def main(g): rates = await g.sys.db.get_all('mysql_8', 'shop', 'rates') rate = rates[0]['per_kg'] if g.req.body.weight > 30: raise g.Errors('Too heavy to ship.', code=422) return {'cost': g.req.body.weight * rate} ``` **TypeScript** **Its test case** ```typescript import * as T from 'types'; import * as assert from 'node:assert'; module.exports = [{ name: 'the cost of a parcel', mocks: [{ method: 'db.getAll', return: [{ per_kg: 2.5 }] }], code: async (g: T.IAMGlobal) => { assert.deepStrictEqual(await g.sys.test.runCustomApi({ name: 'shippingCost', body: { weight: 4 } }), { cost: 10 }); assert.strictEqual(g.sys.test.calls('db.getAll').length, 1); await assert.rejects(g.sys.test.runCustomApi({ name: 'shippingCost', body: { weight: 40 } }), (errors: any) => errors[0].code === 422); }, }]; ``` - What the API raises comes back as its errors : a list in TypeScript, `Errors` in Python, `AmErrors` in PHP, C#, Java and Kotlin, `*am.Errors` in Go, `Errors` in Rust, with the code the API gave. - The coverage of the run has the lines of the Python code, as it was written. ## Testing a utility class A utility class is imported the way custom APIs import it, and its functions are called directly : **TypeScript** ```typescript import * as T from 'types'; import * as assert from 'node:assert'; import * as Tax from 'utils/Tax'; module.exports = [{ name: 'the tax of 100', code: async (g: T.IAMGlobal) => { assert.strictEqual(Tax.add(100), 120); }, }]; ``` - `Tax` is the utility class of [the page of the utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html#other-languages). The `g.sys` calls a class makes are answered by the mocks of the test too. ## Test cases in Python, PHP, C#, Go, Rust, Java and Kotlin Pick the language in the title bar of the editor of the test case. Each language names its tests the way its own test tools do, and what a test raises is its failure : | Language | A test is | It fails when it | |---|---|---| | TypeScript | an object of the list the file exports : `{ name, code, mocks }` | throws (`node:assert`) | | Python | a function whose name starts with `test_`, plain or `async` | raises (`assert`) | | PHP | a function whose name starts with `test_` | throws | | C# | a static method whose name starts with `Test` and which takes `G g` | throws | | Go | an entry of the list `var Tests = []am.Test{ … }` : `{Name: "…", Run: fn}`, `fn` a `func(g *am.G) error` | returns an error, or panics | | Rust | a `fn(g: &mut G) -> Result<(), Errors>` named in `am::tests!(…)` | returns `Err`, or panics (`assert_eq!`) | | Java | a static method whose name starts with `test` and which takes `G g`, in any class | throws (not `assert` : a JVM checks it only with `-ea`) | | Kotlin | a function at the top of the file whose name starts with `test` and which takes `g: G` | throws (`check`, `require`) | Every test gets the same `g` as the code of its language, and `g.sys.test` to run a custom API and to mock : the test case of `shippingCost` above is written in every language. - **`g.sys.test`** has the same calls in every language, named the way the language names things and with its types : `runCustomApi` (`run_custom_api`, `RunCustomApi`), `mock`, `mocks` (several at once), `clearMocks`, `calls`. `runCustomApi` takes `name`, `version`, `body`, `query`, `params` and `headers`, the way the language gives a call its keys (named arguments in Python and PHP, `new RunCustomApi("name") { Body = … }` in C#, `am.RunCustomApi{Name: …}` in Go, `RunCustomApi::new("name").body(…)` in Rust, `new RunCustomApi("name").body(…)` in Java and Kotlin) ; `runCustomApiFull` (`run_custom_api_full`, `RunCustomApiFull`) gives the whole answer : `data`, `statusCode`, `headers`, `logs`. - **Mocks are the same in every language** : `method`, `when` (the argument of the call, or a list of its arguments ; an object matches an argument which has the same keys with the same values, inside objects too), `return` (`returns` where `return` is a word of the language), `error` and `times`. A condition or an answer can be a function of the test, [in every language](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#mocks-in-the-code) : the runner of the test keeps it, and runs it when the mock answers a call. - **The mocks of the Mocks tab work as they are**, in every language. Every test of the file starts with them ; the mocks a test adds in its code come first, and are gone when the next test starts. - **A test case tests code of any language** : a test case in Python can run a custom API written in Go. The API runs in the sandbox of its own language, and the mocks of the test answer its calls of `g.sys` there too. - **Utility classes** are imported the way the other code of the language [imports them](https://docs.apimaker.dev/v4/docs/languages/index.html#utility-classes) : a test case in Python tests a utility class written in Python. - The coverage of a run has the lines of the test case, of the custom APIs it runs and of the utility classes they use, in every language. A file without tests is said in the plan of the run, with how the tests of its language are written. ## Execute / Run - On the page, the play button next to a test case runs that one, **Run** in the editor runs the selected one, **Run all** runs every test case of the account. - The results show test by test : a failed test shows its message, and a click on it shows what was expected and what came, side by side, with the line of the test in the editor. - A run can be cancelled : it stops after the file which is running. - The last run is kept 30 days, so the page shows it again after a reload. **Clear results** above the results of the run panel clears it : its results and its coverage. **Clear coverage** above the coverage clears only the coverage, the results stay. Neither is possible while a run is going on. - A failing test does not stop the file : every test of the file runs and reports. ## Run through the API `POST /api/sites/am-test-cases//run` runs the test cases and answers with the whole run : the plan, the result of every file and test, the totals and the coverage. The body holds `testCaseIds` (every test case when absent), `filter`, `coverage`, `bail` and `timeoutMS`. `POST .../run//cancel` stops a run, `GET .../run/last` gives the last run of the account, `DELETE .../run/last` clears it and `DELETE .../run/last/coverage` clears only its coverage. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/test-cases/test-cases.md) --- # Developer Tools > Swagger docs per API user, TypeScript interfaces from your schemas, client code in 21 languages, a debugger for sandbox code, code search and editing in your own editor. Source: https://docs.apimaker.dev/v4/docs/features/developer-tools.html What API Maker gives a developer besides the APIs : documentation for the people who call them, types for the code which uses them, a debugger, and a way to work from your own editor. ## Swagger docs per API user - Open an API user, enable Swagger docs and generate a **Swagger Token**. **View Swagger JSON** opens its URL. - The docs list only the APIs that the API user can call, like its database, custom and system APIs. - Share the URL with the team or partner that uses that API user. - The URL answers only while Swagger docs are enabled for that API user. Disable them, or generate a new token, to stop an old URL from working. ## TypeScript interfaces from your schemas - Your table schemas become TypeScript interfaces, grouped by instance and database : `db...I
`. See them in `APIs & Logic → Generated Interfaces`. - Import them from `db-interfaces` in your code. ```typescript import * as T from 'types'; import * as db from 'db-interfaces'; async function main(g: T.IAMGlobal) { const products = await g.sys.db.getAll({ instance: 'shop', database: 'main', collection: 'products', }); return products.map(p => p.name); // p is typed } module.exports = main; ``` In Python, PHP, C#, Go, Rust, Java and Kotlin the code editor offers the same columns on the rows a call answers, without interfaces to import : [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor). ## API testing page - Every API of your account in one place: pick an API, get a sample payload, set the headers and tokens, send it and read the response. - `APIs & Logic → API Testing`, or **Test API** on the DB API, Custom API, System API, Events and Schedulers pages. On a listener the body holds the event data; on a scheduler, sending runs the code now. - The request: **Headers**, **Params**, **Query Params**, **Body** (**Generate Data** builds a sample payload from the schema), **Code** (the code of a custom API, listener, scheduler or migration, with its versions), **Pre Hooks** and **Post Hooks** of the API : for an API of a table, the hooks of its instance, its database and its table are listed too, in the order they run (marked **Instance**, **Database**, **Table** : they are changed in their own categories). On a multi-tenant instance, **Select tenant**. - The response: the answer or the errors, with the status code, the time taken and whether it came from the cache; the TypeScript interface of the answer; **Logs** with every `g.logger` line of the code which ran, hooks included. - **Index Maker**, for code (a custom API, a listener, a scheduler, a hook...): every database call of the run. What to look at first (a query reading a whole table, a query in a loop, a query asked twice, a slow or big call), a timeline of the calls, every query shape with its index use, and the indexes to create : [one run from the API testing page](https://docs.apimaker.dev/v4/extensions/index_maker/introduction.html#one-run-from-the-api-testing-page). - A scheduler, a listener, a migration script or a process initializer is sent with the time it runs with (the header `x-am-sandbox-timeout`) : [time of a run](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#time-of-a-run). - **Show Utility Classes** next to the code opens the [utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html#by-language) the code uses : the ones of its language, on its runtime. - Every **Send** is recorded. Save a request as a state to run it again later, star it as a favourite. States go to Git with the project. - Copy the same request as client code from **Integrations**: C, C#, cURL, Dart, Go, HTTP, Java, JavaScript, Kotlin, Node.js, Objective-C, OCaml, PHP, Postman CLI, PowerShell, Python, R, Ruby, Rust, Shell and Swift, with their variants. ## Debugger - Enable debugging in your debug settings : the requests of the API Testing page then run in the **sandbox for debugging** of their environment (one per environment and group of separate sandboxes : one sandbox, one runner), whose runner listens for an inspector. Attach Chrome DevTools (`chrome://inspect`) and stop on breakpoints or on `debugger;` in your code. A request waiting at a breakpoint is not timed out for an hour. - It works for JavaScript and TypeScript on Node.js and on Bun (Bun's web inspector, or the Bun extension of VS Code), for code that runs in a sandbox. The other requests keep using the usual sandboxes. - The port of the inspector is open on the server itself only (127.0.0.1) : reach it through an SSH tunnel. The debug settings show the command of every sandbox for debugging of every server, for example `ssh -L 9229:localhost:41237 root@your-server`, after which `chrome://inspect` finds it on `localhost:9229`. ## Code finder - Search a word in all your code at once: custom APIs, hooks, events, schedulers, migrations, utility classes, schemas, secrets and more, and open the result to edit it. - Learn more [Code finder](https://docs.apimaker.dev/v4/docs/features/team-work.html#code-finder). ## Your own editor - `localClient.js` in your Git repository syncs your files with API Maker both ways. - Generate a sync token in your user profile and set it in `local-client.config.js`. - `npm install` once : the packages your code uses and the declarations of Node.js (`@types/node`), which `tsconfig.json` names for VS Code and WebStorm. - `npm run start` keeps the folder in sync, `npm run test` runs the test cases of the account from the terminal and writes their code coverage to `coverage/lcov.info` for VS Code and WebStorm. Learn more [Test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html). - While it is connected, API Maker also scans the account on its own and keeps `src/api_security_report/` up to date on your disk and in git, so an AI assistant working in the folder reads a fresh report. Learn more [APIs Security Report & Actions](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html). - An AI agent (Codex, Claude Code, Cursor, Copilot) working in the folder reads `AGENTS.md` at its root : every commit writes the guide of API Maker into it. Learn more [AGENTS.md in your repository](https://docs.apimaker.dev/v4/docs/ai/llms.html#agents-md). ```javascript module.exports = () => { return { webSocketURL: 'wss://ws.example.com', // WebSocket URL of your API Maker syncToken: '...', // generate it in your user profile adminUserPath: 'admin', }; }; ``` --- # Browsers in Custom APIs - Playwright and Puppeteer > Run a headless Chromium from API Maker custom APIs to make PDFs, take screenshots or read web pages - Playwright inside a sandbox of its own with a Dockerfile which installs Chromium, or Playwright and Puppeteer on the native process. Source: https://docs.apimaker.dev/v4/docs/guides/browser-automation.html A custom API can drive a headless Chromium with [Playwright](https://playwright.dev/) or [Puppeteer](https://pptr.dev/) : turn HTML into a PDF, take a screenshot, or read a page. A browser needs system libraries next to its npm package, so it runs in one of two places. | | Sandbox | Native process | |---|---|---| | Where the code runs | In sandboxes of a group of its own, in Docker | On the machine of the server (`runOnNativeProcess: true`) | | Where the browser is installed | In the Dockerfile of the group : built once, on every server | On each server, next to API Maker | | Isolation | Your code and the browser stay apart from API Maker | None : the browser shares the server with API Maker | | Set up by | An admin, in the settings of the custom API | The root user of each server | A browser needs memory : plan 2 GB of RAM and 40 GB of disk for the server, and avoid many browsers at the same time. ## Playwright in the sandbox The sandboxes of an account have no browser : nothing is installed that the account did not add. Give the APIs which drive one a [separate sandbox](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html#separate-sandbox) : a group with a Dockerfile which installs Chromium, and the package `playwright` in the same version. The other code of the account keeps its small image. **Basic info : a group of sandboxes with Chromium** ```typescript import * as T from 'types'; let customApi: T.ICustomApiSettingsTypes = { name: 'Invoice PDF', path: '/invoice-pdf', requestMethod: T.ERequestMethod.POST, apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, customApiTimeoutInSeconds: 60, separateSandboxSettings: { enableSeparateSandboxForThis: 'browsers', // every API which names this group shares its sandboxes sandboxCount: 1, packages: { allowAllPackagesOfAdmin: false, sandboxPackages: [{ name: 'playwright', version: '1.63.0' }], }, dockerFileOfSandbox: `FROM {{AM_BASE_IMAGE}} # Chromium and the system libraries it needs, in a folder every user of the image reads : built once, above the packages. ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright RUN npx -y playwright@1.63.0 install --with-deps chromium {{AM_PACKAGES}} {{AM_RUNNER}}`, }, errorList: [], }; module.exports = customApi; ``` **Code : HTML to PDF, sent as a download** ```typescript import * as T from 'types'; const path = require('path'); const { chromium } = require('playwright'); async function main(g: T.IAMGlobal) { const file = `invoice-${g.req.body.number}.pdf`; const browser = await chromium.launch({ args: ['--no-sandbox'] }); try { const page = await browser.newPage(); await page.setContent(`

Invoice ${g.req.body.number}

Total : ${g.req.body.total} EUR

`); await page.pdf({ path: path.join(__dirname, 'uploads', file), format: 'A4', printBackground: true }); } finally { await browser.close(); } // A file of the uploads folder goes out as the answer, and is removed after. return { __am__downloadFilePath: file, __am__downloadFolderFileName: file }; } module.exports = main; ``` - Saving the API builds the image of the group on every server, in the background : about a minute the first time, then never again while the Dockerfile and the packages stay the same. A call which comes before waits for it. - Keep the version of the package and of `playwright install` the same : the package looks for the browsers of its own version. - `--no-sandbox` : the container is the sandbox of Chromium. - More APIs with a browser name the same group and share its image and sandboxes. ## Playwright on the native process 1. Sign in to the server as root (`whoami` prints `root` ; `sudo -s` otherwise) and install Chromium with the libraries it needs : ```bash npx playwright install --with-deps chromium ``` 2. Add the package in the folder API Maker runs in (its `package.json` and `.env` are there) : the runners of the machine find their packages there. ```bash cd /path/of/api-maker npm i playwright ``` 3. Create a custom API and set `runOnNativeProcess: true` in its configuration (the **Basic Info** section of the [custom API](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html#native-process)). 4. Use it in the code. This one writes a PDF from HTML into the `uploads` folder : **Custom API : HTML to PDF with Playwright** ```ts import * as T from 'types'; import { join } from 'path'; const playwright = require('playwright'); async function main(g: T.IAMGlobal) { const browser = await playwright.chromium.launch(); const page = await browser.newPage(); await page.setContent('

Hello from Playwright

', { waitUntil: 'load' }); await page.pdf({ path: join(__dirname, 'uploads', 'playwright.pdf'), format: 'A4', printBackground: true, margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' }, }); await browser.close(); // uploads/playwright.pdf is on the server : upload it to a storage provider or send it as a download. return 'pdf created on the server'; } module.exports = main; ``` > **Tip : Every server** > > What is installed by hand lives on one server : repeat both steps on every server of the cluster, and on each new one. The [sandbox](https://docs.apimaker.dev/v4/docs/guides/browser-automation.html#playwright-in-the-sandbox) has no such step : its image is built on every server from the settings. ## Puppeteer on the native process 1. Sign in to the server as root and install the libraries of Chromium (Ubuntu ; for other systems see [the troubleshooting page of Puppeteer](https://pptr.dev/troubleshooting#chrome-doesnt-launch-on-linux)) : ```bash apt update && apt install -y ca-certificates fonts-liberation libatk1.0-0t64 libatk-bridge2.0-0t64 \ libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 \ libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 \ libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \ libxss1 libxtst6 libasound2t64 lsb-release wget xdg-utils ``` 2. Add the package in the folder API Maker runs in (its `package.json` and `.env` are there) : the runners of the machine find their packages there. ```bash cd /path/of/api-maker npm install puppeteer ``` 3. Set `runOnNativeProcess: true` in the configuration of the custom API, and use it : **Custom API : HTML to PDF with Puppeteer** ```ts import * as T from 'types'; import { join } from 'path'; const puppeteer = require('puppeteer'); async function main(g: T.IAMGlobal) { const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); await page.setContent('

Hello, this paragraph is converted to a PDF using Puppeteer.

', { waitUntil: 'load' }); await page.pdf({ path: join(__dirname, 'uploads', 'puppeteer.pdf'), format: 'A4', printBackground: true }); await browser.close(); // uploads/puppeteer.pdf is on the server : upload it to a storage provider or send it as a download. return 'ok'; } module.exports = main; ``` ## Good to know - Write the files you want to send into the `uploads` folder and return `__am__downloadFilePath` : see [Download file](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.html#download-file). Upload them to a storage service instead when they must be kept. - Close the browser in every path of the code, errors included : an open browser keeps its memory. - The native process runs TypeScript with the `node` of the server : Node.js must be installed there, on every server. - Code on the native process is not isolated from the server : prefer the sandbox when you can, and keep the native process for the APIs which need it. ## Related - [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [How API Maker works](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.html#where-your-code-runs) --- # Languages and Runtimes > Write custom APIs, hooks, schedulers and event listeners of API Maker in TypeScript (Node.js or Bun), Python, PHP, C#, Go, Rust, Java or Kotlin. How code runs, which runtime and version it gets, its packages and how to choose a language. Source: https://docs.apimaker.dev/v4/docs/languages/index.html The code of an account (custom APIs, pre and post hooks, schedulers and event listeners) can be written in any of these languages. Pick the language at the top of the code editor; the code of each item keeps its own language. An API is [one language](https://docs.apimaker.dev/v4/docs/languages/index.html#one-language) : its code and all its hooks are written in the same language and run on the same runtime. | Language | Runtimes | Image (default version) | Packages from | `main` | |---|---|---|---|---| | [TypeScript / JavaScript](https://docs.apimaker.dev/v4/docs/languages/typescript.html) | Node.js, Bun | `node:24-slim`, `oven/bun:1-slim` | npm | `async function main(g)` | | [Python](https://docs.apimaker.dev/v4/docs/languages/python.html) | Python | `python:3.13-slim` | PyPI | `def main(g)` or `async def main(g)` | | [PHP](https://docs.apimaker.dev/v4/docs/languages/php.html) | PHP 8.3+ | `php:8.4-cli` | Packagist (Composer) | `function main($g)` | | [C#](https://docs.apimaker.dev/v4/docs/languages/csharp.html) | .NET | `mcr.microsoft.com/dotnet/sdk:10.0` | NuGet | `public static async Task Main(G g)` | | [Go](https://docs.apimaker.dev/v4/docs/languages/go.html) | Go | `golang:1.27` | Go modules | `func Main(g *am.G) (any, error)` | | [Rust](https://docs.apimaker.dev/v4/docs/languages/rust.html) | Rust | `rust:1-slim` | crates.io | `pub fn main(g: &mut G) -> Result` | | [Java](https://docs.apimaker.dev/v4/docs/languages/java.html) | JDK 20+ | `eclipse-temurin:25-jdk` | Maven Central | `static Object main(G g)` in a class | | [Kotlin](https://docs.apimaker.dev/v4/docs/languages/kotlin.html) | Kotlin on the JDK | `eclipse-temurin:25-jdk`, with the compiler of Kotlin `2.4.20` | Maven Central | `fun main(g: G): Any?` | Every language gets the same [`g`](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) : the request (`g.req`), the answer (`g.res`), the APIs of API Maker (`g.sys.db`, `g.sys.system`, `g.sys.cache`), the logger and the shared values, written the way the language writes things (`g.sys.db.get_all` in Python, `g.Sys.Db.GetAll` in C# and Go). ## The same API in every language The API reads `name` from the body, counts the rows of a table and answers both : **TypeScript** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const count = await g.sys.db.count({ instance: 'mysql_8', database: 'inventory', collection: 'customers' }); return { hello: g.req.body.name || 'world', customers: count }; } module.exports = main; ``` ## How code runs ```mermaid flowchart LR R[Request] --> S[API Maker server] S -->|TypeScript which needs no sandbox| Q[JavaScript engine
inside the server] S -->|your code and its request, once| P[(Sandbox of the account
one per language)] P -->|what the code made of it, once| S P -->|every g.sys call of your code| D[(Databases, Redis)] S -->|the API itself| D ``` - **One sandbox per account and language.** A sandbox is a Docker container of the image of the language (its runtime, its version, the packages you added and your [Dockerfile](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#dockerfile)). The code of an account never shares a process with another account. - **The server answers the request, the sandbox runs your code.**{ #where-code-runs } A request for a custom API, or for an API with hooks, is answered by the server like every other request. Your code goes to the sandbox of its language once, with the request as `g.req`, and what it left comes back once : the parts of the request it changed, and `g.res`. The hooks before the API, the API and the hooks after it each take their turn this way. - **A `g.sys…` call is answered inside the sandbox.**{ #g-sys-in-the-sandbox } Every sandbox loads API Maker itself, through the native package of its language (Node-API for Node.js and Bun ; PyO3 for Python ; JNI for Java and Kotlin ; functions of C for C#, Go, Rust and PHP). `g.sys.db.getAll(…)` in your code is a function call into it : the sandbox reads the database itself, with the groups of the API user of the request, and the server takes no part in the call. This holds for every language ; nothing has to be set, and no setting turns it off. Code on the [native process](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html#native-process) (`runOnNativeProcess: true`) works the same way : the runner of its language on the machine of the server loads API Maker too, one for every account, and answers what the code of each account asks with that account's data. The server answers such a call only where the sandbox can not : in a sandbox for [debugging](https://docs.apimaker.dev/v4/docs/features/developer-tools.html#debugger), for a run of [test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html) (their mocks and their code coverage are with the server), and for a database the sandbox does not reach (a client library or a certificate which is on the machine of the server). - **TypeScript which needs no sandbox runs inside API Maker.**{ #in-the-server } A hook or a custom API in TypeScript which only works on what it is handed (it reads `g.req`, changes it, returns a value, reads through `g.sys.db`, `g.sys.system.getSecret` or `g.sys.cache.getKey`) is run by the server itself, with the JavaScript engine it carries (QuickJS) : no sandbox is asked, none has to start. Code which uses anything else (an npm package, a utility class, `try` / `catch`, dates, timers, `g.shared`, a call which writes) runs in its sandbox of Node.js or Bun, as code in every other language runs in the sandbox of its language. You do not choose : API Maker reads the code when it is saved, and the answer is the same either way. `sandbox.codeInServer: false` in the [settings of the installation](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html#sandboxes) sends all code to its sandbox. - **No network between the server and its sandboxes.** On Linux, where the server runs on the machine of its containers (what the install script sets up), the server reaches each sandbox through a Unix socket in a folder of its own ; everywhere else through the address or the port of the container. Nothing has to be set. - **Many requests at once.** Every runner serves many requests at the same time : coroutines in Python, fibers in PHP, tasks in C#, goroutines in Go, threads in Rust, Java and Kotlin, the event loop in JavaScript. While all runners of a sandbox are busy, the sandbox opens another one, and while all sandboxes are busy a new sandbox starts, up to the [sandbox count](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#sandbox-count). - **Compiled once.** TypeScript is compiled to JavaScript when you save. Go, Rust, C#, Java and Kotlin code is compiled the first time it runs; since saving starts that compilation in the background, the first request does not wait for the compiler, and code which does not compile is reported in the console of the account a few seconds after the save. Python and PHP code is ready at once. - **Timeouts.** A run which takes longer than the timeout of its request takes its sandbox with it : the next request gets a new sandbox. Code which loops forever can not block the others. - **Images are shared.** An image is named after what it is made of, so every account with the same runtime, version, packages and Dockerfile uses the same image, built once per server. Saving the settings builds the images which changed on every server, in the background. ## The same API, measured in every language One custom API which reads the ten rows of a table (`g.sys.db.getAll`), written in every language. Ten connections, 20 seconds each after a rest, the sandboxes in Docker on a laptop (2026-10-09) ; no request failed. | Language | Requests a second | |---|---| | TypeScript on Node.js | 11,648 | | TypeScript on Bun | 11,268 | | Rust | 11,260 | | Kotlin | 11,230 | | Java | 11,042 | | PHP | 10,507 | | C# | 10,338 | | Go | 9,957 | | Python | 9,415 | The numbers of your machine differ ; what holds is that the language is a small part of what a request costs. Most of it is API Maker and the database, which every language shares. ## Choosing the language and the runtime of a piece of code 1. Open the custom API, hook, scheduler, event listener, WebSocket event, migration script, process initializer, token generator (its functions), utility class or test case. [Settings](https://docs.apimaker.dev/v4/docs/languages/index.html#basic-info) have no language to pick : they are TypeScript. 2. Pick the **language** in the title bar of the editor. A language with several runtimes (TypeScript : Node.js, Bun) shows a second list : pick the **runtime**, or leave **Default** to run on the one [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#runtime) names for the language. 3. Untouched code becomes the template of the new language. Code you wrote is kept unless you ask for the template : rewrite it in the new language. 4. Save. The code is saved as it is written : `codeLanguage` remembers the language and `codeRuntime` the runtime (absent for the default one), and its versions keep them too. - **One account, many languages.** Every item has its own language : one custom API in TypeScript, the next in Python, a scheduler in Go, an event listener in Kotlin. A custom API can call another custom API written in another language ; each runs in the sandbox of its language. - **One API, one language**{ #one-language } : the code of an API and all its hooks (before and after it, of every version) are written in one language and run on one runtime : a request has one kind of code, in one place. For the APIs of tables, the hooks of the instance, of the database, of the table and of the API run for the same request, so they are one language too. - The form of a hook shows the language of its API, with a lock : there is nothing to choose. The first hook of an API which has no code of its own (an API of a table, a system API) chooses, and the next ones follow it. - A custom API which has hooks keeps its language : rewrite or remove the hooks first. The admin panel says which hooks stand in the way. - A save which would mix two languages is refused in words, wherever it comes from (the admin panel, a pull of git, the local client). - **Logos.** The lists show the language of every piece of code with its logo, and the runtime too where a language has several (TypeScript on Node.js or Bun) : the grid and the tree of the custom APIs, the hooks of an API, schedulers, event listeners, process initializers, migration scripts, token generators, test cases, and the Code tab of API Testing. - **Two runtimes side by side.** One custom API on Node.js and another on Bun run in the same account, each in the image of its runtime with the packages of JavaScript. - **Settings are always TypeScript**{ #basic-info } : code which describes something, and runs once when you save it, is written in TypeScript, whatever language the code next to it is written in. API Maker [runs it itself](https://docs.apimaker.dev/v4/docs/languages/index.html#settings-run) at that moment, in no sandbox and on no runtime : what settings export is JSON for the database. - What is settings : the Basic Info of a custom API (its [settings](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html)) and of a token generator, the settings of the APIs of a [table](https://docs.apimaker.dev/v4/docs/settings/apiSettings.html), of a [database](https://docs.apimaker.dev/v4/docs/settings/databaseSettings.html) and of a [system API](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html), a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html), the texts of [internationalization](https://docs.apimaker.dev/v4/docs/i18/i18.html) and a [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html). - Their editors have no language to pick. The Basic Info tab, which stands next to code of any language, shows TypeScript with a lock : there is nothing to choose, and no runtime. - In git they are `.ts` files, next to the code where there is code (`orders.config.ts` next to `orders.py`). - What the code exports is what API Maker keeps : a request reads it, and never runs settings again. - Settings import `types` and your own utility classes written in TypeScript, not packages : see [where settings run](https://docs.apimaker.dev/v4/docs/languages/index.html#settings-run). - **API Testing follows the language.** The code editor of the API Testing page shows a custom API, a scheduler, a listener or a hook in the language it is written in, and **SEND** tries the code on screen in that language, on the runtime it names. - A language or a runtime API Maker does not run, and a runtime of another language (`codeLanguage: 'python'` with `codeRuntime: 'node'`), are refused when the code is saved, in words. ## Where settings run Settings describe an item : what they export is kept as JSON, and nothing else happens. So API Maker runs them itself, inside the server, the moment you save, with the JavaScript engine it carries (QuickJS). No sandbox starts for them and no image is built : a save answers in milliseconds, also for an account whose code is all Python, also while the images of its sandboxes are still being built. | Settings can use | | |---|---| | TypeScript itself | Everything of the language : objects, functions, classes, `async`, `Date`, `JSON`, `Math`, regular expressions, `BigInt`. | | `types` | `import * as T from 'types'` : the enums and the interfaces of API Maker. | | Your utility classes | The ones written in TypeScript : `import { ROLES } from 'utils/Roles'`. For the lists, the names and the small functions several settings share. A class which itself imports a package can not be used by settings. | | A few things of Node.js | `console` (it prints into the console of the account), `Buffer`, the timers, `crypto` (`randomUUID`, `randomBytes`, `randomInt`, `createHash`, `createHmac`), `util.format`. `process.env` is there, and empty : settings see nothing of the machine. | | Settings can not use | | |---|---| | Packages | The packages of npm are in the sandboxes, for code which runs. `import * as _ from 'lodash'` in settings is refused when you save : `Cannot find module 'lodash' : settings are run by API Maker itself when they are saved, not in a sandbox…` | | The rest of Node.js | No files and no network : no `fs`, `path`, `http`, `fetch`. Nor what Node.js adds to the language : `URL`, `structuredClone`, `atob` / `btoa`, `Intl` (texts are compared letter by letter). | | `g.sys` | Settings call no API. They describe ; a [custom API](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html) or a [hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) does. | **utils/Roles : a utility class in TypeScript, shared by settings** ```typescript export const ROLES: string[] = ['admin', 'editor', 'reader']; ``` **A schema which uses it** ```typescript import { ISchemaType, EType } from 'types'; import { ROLES } from 'utils/Roles'; let schema: ISchemaType = { name: { __type: EType.string, validations: { required: true } }, role: { __type: EType.string, validations: { enum: ROLES } }, }; module.exports = { schema }; ``` - **The editor of settings knows what settings have.** The Basic Info tab of a custom API and of a token generator, and the editors of API settings, schemas, internationalization and secrets offer what the table above lists, and nothing else of Node.js : after `crypto.` the six functions which are there, after `process.` `env` and no `argv`, in `import … from '` the modules `crypto`, `buffer`, `util` and your utility classes. A package of npm, `fs`, `fetch` or `URL` is underlined in red while you type, and the editor says why : `Settings are run by API Maker itself when they are saved, not in a sandbox…` The Code tab next to it keeps Node.js and the packages of your sandbox : that code runs there. One name is offered which settings do not have : `Intl`, which belongs to the language itself for the editor. - **Limits** : settings have 10 seconds and 64 MB. Code which does not end is stopped, and the save says so : `The schema did not finish in 10 seconds…` - **Errors** : what settings throw is the error of the save, with the line of your TypeScript (`at fn (MainSandbox.ts:7:24)`). - **Functions inside settings** : the functions of a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) (`validatorFun`, `conversionFun`, `defaultFun`) are kept as their text when the schema is saved. They run later, with the requests, [where the request is answered](https://docs.apimaker.dev/v4/docs/schema/schema.html#where-the-functions-run), and there they have the packages of that sandbox : call `require('dayjs')` inside the function, not at the top of the schema. - **Dates** are counted in the time zone of the server. Write a moment with its zone (`'2026-01-02T00:00:00+05:30'`) or with `Date.UTC(…)`. ## The code editor in every language The code editor completes code in every language, not only in TypeScript. It knows `g` as the language writes it, with the types of every call : what a call takes, what it answers, your instances, databases and tables, and the columns of a table on the rows a call answers. - **`g` and its parts.** After `g.` (`$g->` in PHP) the editor offers `req`, `res`, `sys`, `logger` and `shared`, then the calls of each part of `g.sys` under the names of the language : `get_all` in Python and Rust, `GetAll` in C# and Go, `getAll` in PHP, Java and Kotlin. Each shows its signature with the types of the language and says what it does ; the hint which opens at `(` shows what it takes, and so does pointing at a name. `full` (after `g.sys.db.`, `g.sys.system.`, `g.sys.cache.`) has the same calls with their whole answers. - **The request and the answer.** After `g.req.` and `g.res.` the editor offers a member for each of their keys, as the language reads one : a property (`g.req.body` in Python, PHP and Kotlin, `g.Req.Body` in C#), a method (`g.Req.Body()` in Go, `g.req.body()` in Rust), a getter and a setter (`g.req.getBody()`, `g.res.setStatusCode(201)` in Java). After `auth` it offers the opened tokens. - **What a call takes.** Each language makes it its own way, and the editor follows : | Language | What a get all takes | The editor offers | |---|---|---| | TypeScript | `{ instance: 'mysql_8', …, queryParams: { limit: 10 } }` | the keys of the object | | Python | `get_all('mysql_8', 'inventory', 'customers', limit=10)` | the named arguments : `limit=`, `find=`, `sort=`… | | PHP | `getAll('mysql_8', 'inventory', 'customers', limit: 10)` | the named arguments : `limit: `, `find: `, `sort: `… | | C# | `new GetAll("mysql_8", "inventory", "customers") { Limit = 10 }` | the properties between the braces : `Limit = `, `Find = `… | | Go | `am.GetAll{Instance: "mysql_8", …, Limit: 10}` | the fields between the braces : `Limit: `, `Find: `… | | Rust | `GetAll::new("mysql_8", "inventory", "customers").limit(10)` | `new` after `GetAll::`, then the methods which set a key | | Java | `new GetAll("mysql_8", "inventory", "customers").limit(10)` | the methods which set a key, one after the other | | Kotlin | `GetAll("mysql_8", "inventory", "customers").limit(10)` | the methods which set a key, one after the other | - Each key says what it is for, in the words of TypeScript. Where a key names an instance, a database or a table, the editor offers the ones of your account : the instances, then the databases of the instance, then the tables of the database. Where a key names columns (`find`, `select`, `sort`, `saveData`, `updateData`, the column of a `distinct`), it offers the columns of the table. - **What a call answers has its type**, and the editor offers its members : the columns of the table on a row of `getAll` and of `query`, on the row of `getById`, `updateById`, `replaceById` and `removeById`, on what `saveSingleOrMultiple` saved (`saved.row`, `saved.rows`), on the rows a stream hands to its function and on the deleted rows of `removeByQuery` ; the keys of what an API describes (`updatedRowsCount` of an `updateMany`, `token` of a `getToken`…) ; `success`, `data`, `errors` on the whole answer of a call. A column which is an object offers its own columns. Each column shows its type, and whether it is required or the primary key. | Language | The columns are offered after | |---|---| | TypeScript | `rows[0].` | | Python | `rows[0].`, `row['`, `row.get('` | | PHP | `$rows[0]->`, `$row['`, `$row->get('` | | C# | `rows[0].` (written `rows[0]["first_name"]`), `row["`, `row.GetString("` (and `GetInt64`, `GetDouble`, `GetObject`…) | | Go | `rows[0].` (written `rows[0]["first_name"]`), `row["`, `row.String("` (and `Get`, `Map`, `List`, `Float`, `Int`, `Bool`) | | Rust | `rows[0].` (written `rows[0]["first_name"]`), `row["`, `row.get("` | | Java | `rows.get(0).` (written `.get("first_name")`), `row.getString("` (and `getLong`, `getDouble`, `getObject`…) | | Kotlin | `rows[0].` (written `rows[0]["first_name"]`), `row["`, `row.getString("` | - **Where the table is read from.** From the names written as text : in the call itself (what it is given first, a named argument, a method, a field), in a variable what the call takes is kept in, or in a function of the code which makes it. A loop over the rows, a row kept in a variable and a function given to a stream keep the table. - **What is not known.** Names made while the code runs, and a table without a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html), offer no columns : the editor then suggests the words of the code, as it did. - The editor asks the server for your tables each time it opens ; **↺ Types** in its corner reads them again after a schema changed. ## Utility classes in every language A [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) is code the other code of its language shares. Pick its language in the title bar of its editor, as for any other code. Each language names what it imports its own way, and it is the same way in a sandbox and in the [Git repository](https://docs.apimaker.dev/v4/docs/Git/git.html) of the account : | Language | A class `Tax` in the folder `utils/` is | The code names it | |---|---|---| | TypeScript | the module `utils/Tax` | `import * as tax from 'utils/Tax';` | | Python | the module `Tax` | `from Tax import add` | | PHP | what its file declares : classes, functions, constants | `Tax::add(100)`, `add_tax(100)`, `TAX_RATE` | | C# | what its file declares, compiled with the code | `Tax.Add(100)` | | Go | a package, imported by its folder in the repository | `import tax "apimaker/src/utility_classes/utils---Tax"` | | Rust | the module `Tax` of the crate of the code | `use crate::Tax::add;` | | Java | the class `Tax`, compiled with the other classes of Java of the account | `Tax.add(100)` | | Kotlin | what its file declares (it starts as an `object Tax`), compiled with the other classes of Kotlin | `Tax.add(100.0)` | - **A class is used by the code of its own language.** Python code imports the classes written in Python, Go code the ones written in Go, and Java code does not see a class written in Kotlin. Code of another language reaches what a class does through a custom API. - **Saved is live.** The next request runs with the class as it was saved : nothing restarts. Go, Rust, C#, Java and Kotlin code which uses the class is compiled again on its next run. - **Classes use each other**, and the packages of their language. - **Names.** In Python and Rust the name of the class is the name of its module : letters of a to z, digits and `_`, no word of the language (`lambda`, `type`), and one class of that name per language. In Go the folder is what is imported : letters of a to z, digits, `.`, `-` and `_`. In Java and Kotlin it is the name of a class or of an object : letters of a to z, digits and `_`, no digit first, no word of the language (`record`, `object`), not `am`, and one class of that name per language. A folder and a name belong to one class, whatever its language. What does not fit is refused when the class is saved, in words. - **What a class keeps.** A module of Python, a file of PHP, a package of Go and the classes of Java or of Kotlin are loaded once per sandbox : what they keep (a counter, a connection) is shared by all code of the account there. A class of C# or of Rust is compiled into every piece of code which runs : what it keeps in a static is kept per piece of code. - **PHP** : a utility class has no `namespace` of its own, and the code names what it declares without a leading `\`. API Maker loads every class in a namespace it chooses, so a class which changed is loaded next to the one it was. - **The repository resolves the same names** : `pyproject.toml` lists the folders of the Python classes for Pyright / Pylance, pytest and mypy, `composer.json` loads the files of the PHP classes, `am/rust/project.rs` declares the modules of Rust, and the projects of Gradle (`am/java/build.gradle.kts`, `am/kotlin/build.gradle.kts`) compile the classes of Java and of Kotlin together, then every piece of code with them. Go and C# need nothing : a folder is a package, and one project compiles every file. ## Test cases in every language A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) is written in any of the languages too. Its tests are named the way the test tools of the language name them (`test_…` functions in Python and PHP, `Test…` methods in C#, a list `Tests` in Go, `am::tests!` in Rust, `test…` methods in Java and functions in Kotlin), each gets `g`, and `g.sys.test` runs a custom API of any language with the mocks of the test. > **What stays TypeScript** > > - [Settings](https://docs.apimaker.dev/v4/docs/languages/index.html#basic-info) : the Basic Info of a custom API and of a token generator, the settings of the APIs of a table, of a database and of a system API, schemas, the texts of internationalization and secrets. TypeScript, whatever language the code is written in, [run by API Maker itself](https://docs.apimaker.dev/v4/docs/languages/index.html#settings-run). > - The functions of schemas (`validatorFun`, `conversionFun`, `defaultFun`), and mocks which are functions. A request answered in the sandbox of another language runs the functions of schemas in that sandbox too, in the JavaScript engine API Maker has there : see [where the functions run](https://docs.apimaker.dev/v4/docs/schema/schema.html#where-the-functions-run). > - In a run of test cases, the lines of utility classes are counted for the classes written in TypeScript. ## Versions API Maker runs the versions of a runtime released after January 2023. Where the runner of a language needs a newer one, that one is the oldest : Go 1.21 and Rust 1.88. | Runtime | Default version | Oldest version | |---|---|---| | Node.js | `24` | `20` | | Bun | `1` | `1.0` | | Python | `3.13` | `3.12` | | PHP | `8.4` | `8.3` | | .NET (C#) | `10.0` | `8.0` | | Go | `1.27` | `1.21` | | Rust | `1` (the latest stable) | `1.88` | | JDK (Java and Kotlin) | `25` | `20` | - The version of a runtime is picked in [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#runtime), from the list of the versions API Maker runs. - An older version is refused when the settings are saved, in words : `Python 3.10 is too old : API Maker runs Python 3.12 and later.` - Code on the [native process](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html#native-process) runs with the runtime of the machine of the server, at least the oldest version above : [API Maker Launchpad](https://docs.apimaker.dev/v4/docs/getting-started/local-run.html#languages-for-native-apis) installs one, on your computer and on your servers. ## Packages Each language installs its packages from its own registry, in its own image : nothing is installed unless you add it in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages). The code imports them the usual way : | Language | Add in settings | In the code | |---|---|---| | TypeScript | `dayjs` `1.11.13` | `import * as dayjs from 'dayjs';` | | Python | `requests` `2.32.3`, with extras `uvicorn[standard]` | `import requests` | | PHP | `guzzlehttp/guzzle` `^7.9` | `use GuzzleHttp\Client;` | | C# | `Humanizer.Core` `2.14.1` | `using Humanizer;` | | Go | `github.com/google/uuid` `v1.6.0` | `import "github.com/google/uuid"` | | Rust | `serde[derive]` `1`, `uuid[v4]` `1` | `use uuid::Uuid;` | | Java | `com.google.code.gson:gson` `2.11.0` | `import com.google.gson.Gson;` | | Kotlin | `com.google.code.gson:gson` `2.11.0` | `import com.google.gson.Gson` | - A version is optional : the latest one is installed when the image is built. - While code runs nothing is downloaded : Bun's automatic install and Go's module proxy are switched off. A package which is not installed is said in the error : `the package 'github.com/google/uuid' is not installed : add it to the packages of Go in the settings of the sandbox`. ## Errors in every language What the code raises becomes the errors of the answer, with the lines of your code : **A Python ValueError, as the API answers it** ```json { "success": false, "statusCode": 500, "errors": [{ "code": 500, "message": "ValueError: quantity must be positive", "stack": [ "ValueError: quantity must be positive", " File \"orders.py\", line 6, in check", " raise ValueError('quantity must be positive')" ] }] } ``` To answer with your own errors and status, use the error type of the language : `throw [{ code: 409, message }]` in TypeScript, `raise g.Errors('…', code=409)` in Python, `throw new AmErrors('…', 409)` in PHP, C# and Java, `return nil, am.Fail(409, "…")` in Go, `Err(am::fail(409, "…"))` in Rust, `throw AmErrors("…", 409)` in Kotlin. ## Bytes as the answer in every language A file, a picture, a PDF : code which returns bytes (or leaves them in the output of `g.res`) answers with those bytes as they are, under the content type it names (`image/png`, `application/pdf`…), with the headers it sets (`Content-Disposition`, `Cache-Control`). Each language has its own type of bytes : | Language | Bytes as the answer | |---|---| | TypeScript / JavaScript | a `Buffer`, or any `Uint8Array` | | Python | `bytes`, `bytearray`, `memoryview` | | PHP | `new AmBytes($bytes)` : a text of PHP holds bytes as well as letters | | C# | a `byte[]` or a `Memory` returned by `Main` ; `g.Res.Output = Json.Bytes(bytes)` | | Go | a `[]byte` | | Rust | `am::bytes(bytes)` : a value of JSON has nothing which stands for bytes | | Java | a `byte[]` | | Kotlin | a `ByteArray` | The page of each language has an example, and [the content types](https://docs.apimaker.dev/v4/examples/res/res.html#octet-stream) one in every language. ## Related - [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) · [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html) · [Separate sandboxes of a custom API](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html#separate-sandbox) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/languages/index.md) --- # TypeScript and JavaScript > Write custom APIs, hooks, schedulers and event listeners of API Maker in TypeScript, run by Node.js or Bun. async function main(g), g.req, g.res, g.sys.db, utility classes, npm packages, with many examples. Source: https://docs.apimaker.dev/v4/docs/languages/typescript.html Code in TypeScript is compiled to JavaScript when you save it, and runs on the runtime you choose in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) : **Node.js** (the default) or **Bun**, in the version you pick there, with only the npm packages you add. **hello.ts** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { return { hello: g.req.body.name || 'world' }; } module.exports = main; ``` | | Node.js | Bun | |---|---|---| | Image | `node:-slim` (24) | `oven/bun:-slim` (1) | | Oldest version | 20 | 1.0 | | Packages | `npm install` | `bun add` | | Code coverage of test cases | yes | no : Bun has no V8 profiler | | Downloads while code runs | none | none (automatic install off) | The code, the [global object `g`](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html), utility classes, `types` and the [examples of `g.sys`](https://docs.apimaker.dev/v4/examples/index.html) are the same on both : switching the runtime needs no change of code, unless the code uses something only one runtime has (`Bun.file`). ## The request and the answer **Read the request, shape the answer** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const status = g.req.query.status || 'open'; const signed = !!g.req.headers['x-am-authorization']; g.res.statusCode = T.EStatusCode.CREATED; g.res.headers = { 'X-Request-Kind': 'orders' }; return { customer: g.req.params.id, status, signed }; } module.exports = main; ``` ### Bytes as the answer A file, a picture, a PDF : return its bytes (a `Buffer`, or any `Uint8Array` : `Buffer.from(await answer.arrayBuffer())` for what `fetch` downloaded), or put them in `g.res.output`, and name their kind of content. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. (A text under a kind of bytes, `image/gif`, is read as base64.) **A picture as the answer** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const picture = Buffer.from('R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=', 'base64'); g.res.contentType = 'image/gif' as T.EContentType; g.res.headers = { 'Content-Disposition': 'inline; filename="pixel.gif"' }; return picture; } module.exports = main; ``` ## Calling API Maker : `g.sys` **Database calls** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const table = { instance: 'mysql_8', database: 'inventory', collection: 'customers' }; const rows = await g.sys.db.getAll({ ...table, queryParams: { limit: 10, sort: '-customer_id' } }); const two = await g.sys.db.getById({ ...table, id: 2 }); const count = await g.sys.db.count({ ...table, find: { country: 'IN' } }); return { first: rows[0], two, inIndia: count }; } module.exports = main; ``` **Calls at the same time** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const shop = { instance: 'mongodb', database: 'shop' }; const [orders, products] = await Promise.all([ g.sys.db.count({ ...shop, collection: 'orders' }), g.sys.db.count({ ...shop, collection: 'products' }), ]); return { orders, products }; } module.exports = main; ``` **Rows as they arrive** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { let amount = 0; await g.sys.db.getAllByStream({ instance: 'mysql_8', database: 'inventory', collection: 'orders' }, (row: any) => { amount += row.amount || 0; }); return { amount }; } module.exports = main; ``` Every call of `g.sys` has its own page of examples : [database](https://docs.apimaker.dev/v4/examples/sys/db/getAll.html), [generated APIs](https://docs.apimaker.dev/v4/examples/sys/db/getAll.html#without-a-schema), [system APIs](https://docs.apimaker.dev/v4/examples/sys/system/system.html), [cache](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.html#from-code). ## Errors Throw a list of errors to answer with them : **Refuse a request** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { if (!g.req.body.quantity) { throw [{ code: T.EStatusCode.BAD_REQUEST, message: `'quantity' is missing.` }]; } return { ok: true }; } module.exports = main; ``` **Catch the errors of a call** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { try { await g.sys.db.removeById({ instance: 'mysql_8', database: 'inventory', collection: 'customers', id: 404 }); return { removed: true }; } catch (errors) { g.logger.warn('not removed : ', errors[0].message); return { removed: false, code: errors[0].code }; } } module.exports = main; ``` An exception answers `500` with its message and the lines of your TypeScript (the source map of the save is applied). ## Logs, printing and shared values **Logs and shared values** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { g.logger.log('order ', 7, ' paid'); console.log('kept in the console of the account'); g.shared.calls = (g.shared.calls || 0) + 1; // lives as long as the runner g.res.shared.started = true; // travels with this request : pre hook → API → post hook return g.shared.calls; } module.exports = main; ``` ## Hooks **Pre hook : only my rows** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const person = g.req.auth?.authAMDB || {}; g.req.query.find = { ...(g.req.query.find || {}), owner_id: person.id }; } module.exports = main; ``` **Post hook : hide a column** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { g.res.output = (g.res.output || []).map(({ password, ...row }: any) => row); } module.exports = main; ``` ## Packages Add the package in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages) (`dayjs` `1.11.13`), then import it : **An npm package** ```typescript import * as T from 'types'; import * as dayjs from 'dayjs'; async function main(g: T.IAMGlobal) { return { tomorrow: dayjs().add(1, 'day').format('YYYY-MM-DD') }; } module.exports = main; ``` - Versions are what npm takes : `4.17.21`, `^4.17.0`, `latest`. - Nothing is installed unless you add it : a new sandbox has only the packages of its runtime. - Types of the packages are read by the code editors from the sandbox. ## Utility classes Code in TypeScript imports the [utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in TypeScript on both runtimes (`import { Tax } from 'utils/Tax';`), and they import each other and your packages. A class written in [another language](https://docs.apimaker.dev/v4/docs/languages/index.html#utility-classes) is for the code of that language. ## How it runs - The code is compiled to JavaScript when you save it; the runner keeps the compiled function by the hash of the code. - One runner serves many requests at once on its event loop; the sandbox opens more runners while all are busy. - Values keep their kind inside one sandbox (dates are `Date`, ids are `ObjectId`); a request which comes from outside reaches the code as JSON. ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) · [Code examples](https://docs.apimaker.dev/v4/examples/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) --- # Python > Write custom APIs, hooks, schedulers and event listeners of API Maker in Python. main(g) as a function or a coroutine, g.req, g.res, g.sys.db, g.sys.system, g.sys.cache, the logger, errors, packages from PyPI, with many examples. Source: https://docs.apimaker.dev/v4/docs/languages/python.html Code in Python defines `main(g)`, as a function or as a coroutine. It runs on the official image of Python (`python:3.13-slim` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) name another version), with only the packages you add from PyPI. **hello.py** ```python def main(g): return {'hello': g.req.body.name or 'world'} ``` | | | |---|---| | Runtime | CPython 3.12 or later, a version of `python:-slim` | | `main` | `def main(g)` runs on a thread of its own; `async def main(g)` runs on the event loop of the runner | | Calls of `g.sys` | typed : named arguments, each with its type (`get_all("mysql_8", "inventory", "customers", limit=10)`), and what a call answers has its type. They answer at once in a function, are awaited in a coroutine | | Answer | what `main` returns; `None` lets the API go on | | Values | plain JSON : objects are `dict`s whose keys are also attributes (`g.req.body.id`), a date is a text | | Packages | PyPI : `requests`, `pandas>=2,<3`, extras like `uvicorn[standard]` | | Printing | `print()` goes to the console of the account | ## The request and the answer `g.req` and `g.res` have a property for each key they are known to have (`g.req.body`, `g.req.query`, `g.res.status_code`), and are dictionaries of JSON whose keys are also attributes. A key which is not there is `None`, as it is `undefined` in JavaScript, so `g.req.body.name or 'world'` needs no check. **Read the request, shape the answer** ```python def main(g): customer_id = g.req.params.id # /api/custom-api//orders/:id status = g.req.query.status or 'open' # ?status=paid token = g.req.headers['x-am-authorization'] # headers are in small letters g.res.status_code = 201 g.res.headers['X-Request-Kind'] = 'orders' return {'customer': customer_id, 'status': status, 'signed': token is not None} ``` - Change `g.req` in a pre hook : the API sees the change. Only what you change goes back to the server. - `g.res.output` holds the answer of the API in a post hook. - `g.req.auth` has the opened tokens : `authAMUser`, `authAMDB` (the row of your users table)… ### Bytes as the answer A file, a picture, a PDF : return its bytes (`bytes`, `bytearray`, `memoryview`), or put them in `g.res.output`, and name their kind of content. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. (A text under a kind of bytes, `image/gif`, is read as base64.) **A picture as the answer** ```python import base64 def main(g): picture = base64.b64decode('R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=') g.res.content_type = 'image/gif' g.res.headers['Content-Disposition'] = 'inline; filename="pixel.gif"' return picture ``` ## Calling API Maker : `g.sys` The methods of `g.sys.db`, `g.sys.system` and `g.sys.cache` have their Python names (`get_all`) and take named arguments, each with its type : what a call is about comes first and needs no name (the instance, the database, the table), the rest is given by name. What a call answers has its type : rows are a `list[Row]` (a `dict` whose keys are attributes too), a row a `Row` or `None`, a count an `int`, what an API describes a class with a property for each key (`changed.updated_rows_count`). A name which no call has is a `TypeError` of Python : a key is never sent under a wrong name. **Database calls in a coroutine** ```python from am import G async def main(g: G): rows = await g.sys.db.get_all('mysql_8', 'inventory', 'customers', limit=10, sort='-customer_id') one = await g.sys.db.get_by_id('mysql_8', 'inventory', 'customers', 2) count = await g.sys.db.count('mysql_8', 'inventory', 'customers', find={'country': 'IN'}) return {'first': rows[0] if rows else None, 'two': one, 'in_india': count} ``` The same in a function : calls answer at once. (`from am.sync import G` tells an editor of Python so : the same `g`, with calls which are not awaited.) **Database calls in a function** ```python from am.sync import G def main(g: G): table = ('mysql_8', 'inventory', 'customers') saved = g.sys.db.save_single_or_multiple(*table, {'first_name': 'Ada', 'last_name': 'Lovelace'}) g.sys.db.update_by_id(*table, 2, update_data={'first_name': 'MALLORY'}) g.sys.db.remove_by_id(*table, 101) return saved.row # what a save answers : saved.row, saved.rows, saved.is_list ``` What a call takes is also a class of `am`, made the same way : it is given in place of the named arguments, kept in a variable, and it is what the calls of several take. A `dict` with the keys as the API takes them is taken too (what a request brought). **What a call takes, as a value** ```python from am import G, GetAll, GetToken async def main(g: G): customers = GetAll('mysql_8', 'inventory', 'customers', find={'country': 'IN'}, limit=10) rows = await g.sys.db.get_all(customers) same = await g.sys.db.get_all({'instance': 'mysql_8', 'database': 'inventory', 'collection': 'customers'}) tokens = await g.sys.system.get_tokens([GetToken('ada', 'secret'), GetToken('bob', 'secret')]) whole = await g.sys.db.full.get_all(customers, get_total_count=True) # the whole answer : success, data, errors… return {'rows': len(rows), 'same': len(same), 'tokens': len(tokens), 'ok': whole.success} ``` Run calls at the same time with `asyncio.gather` : **Calls at the same time** ```python import asyncio async def main(g): orders, products = await asyncio.gather( g.sys.db.count('mongodb', 'shop', 'orders'), g.sys.db.count('mongodb', 'shop', 'products'), ) return {'orders': orders, 'products': products} ``` ### Rows as they arrive `get_all_by_stream` and `query_by_stream` take what is asked (a `GetAll`, a `Query`) and hand the rows to your function as the database sends them : thousands of rows never sit in memory at once. The function can be a coroutine too. **Sum a large table** ```python from am import GetAll def main(g): total = {'amount': 0, 'rows': 0} def take(row): total['amount'] += row.amount or 0 total['rows'] += 1 g.sys.db.get_all_by_stream(GetAll('mysql_8', 'inventory', 'orders'), take) return total ``` ### Aggregations, generated APIs, plain queries **Other database calls** ```python async def main(g): by_status = await g.sys.db.aggregate('mongodb', 'shop', 'orders', aggregate_query=[{'$group': {'_id': '$status', 'n': {'$sum': 1}}}]) logs = await g.sys.db.gen.get_all_gen('mysql_8', 'inventory', 'logs') plain = await g.sys.system.execute_query('postgresql', 'SELECT 1', database='crm') return {'by_status': by_status, 'logs': len(logs), 'plain': plain} ``` ### System APIs and cache **Encrypt, hash, events and cache keys** ```python async def main(g): sealed = await g.sys.system.encrypt({'card': '4111'}) digest = await g.sys.system.hash('my text') await g.sys.system.emit_event('order_created', {'id': 7}, ['notify_shop']) await g.sys.cache.set_key('last_order', '7', ttl_seconds=600) last = await g.sys.cache.get_key('last_order') return {'sealed': sealed, 'digest': digest, 'last': last} ``` **Call another service from the sandbox** ```python async def main(g): answer = await g.sys.system.call_external_api('https://api.example.com/rates', method='GET', timeout=5000, headers={'accept': 'application/json'}) return answer.body # answer.status_code, answer.headers, answer.body ``` ## Errors Raise `Errors` (`from am import Errors`, also `g.Errors`) to answer with your own errors and status : **Refuse a request** ```python def main(g): if not g.req.body.quantity: raise g.Errors("'quantity' is missing.", code=400) if g.req.body.quantity > 100: raise g.Errors([{'code': 400, 'message': 'At most 100 at a time.'}, {'code': 400, 'message': 'Split the order.'}]) return {'ok': True} ``` A call of `g.sys` which fails raises `Errors` with the errors of the server. Catch it to decide what to do : **Catch the errors of a call** ```python from am import Errors async def main(g): try: await g.sys.db.remove_by_id('mysql_8', 'inventory', 'customers', 404) except Errors as refused: g.logger.warn('not removed :', refused.message) # refused.code, refused.errors[0].field… return {'removed': False, 'why': refused.message, 'code': refused.code} return {'removed': True} ``` Any other exception answers `500` with its type, message and the lines of your code (`ValueError: quantity must be positive`, `KeyError: the key 'qty' is not there`). Calling `sys.exit()` is refused in words : return the answer instead. ## Logs, printing and shared values **Logs and shared values** ```python def main(g): g.logger.log('order', 7, 'paid') # one line in the logs of the answer : "order 7 paid" g.logger.error({'step': 'payment', 'ok': False}) # objects as indented JSON print('kept in the console of the account') g.shared['calls'] = g.shared.get('calls', 0) + 1 # lives as long as the runner : shared by every request it serves g.res.shared.started = True # travels with this request : pre hook → API → post hook return g.shared['calls'] ``` ## Hooks A pre hook which limits every query of a table to the rows of the caller (row level security) : **Pre hook : only my rows** ```python def main(g): person = (g.req.auth or {}).get('authAMDB') or {} find = g.req.query.find or {} find['owner_id'] = person.get('id') g.req.query.find = find ``` A post hook which shapes the answer of the API : **Post hook : hide a column** ```python def main(g): rows = g.res.output or [] for row in rows: row.pop('password', None) g.res.output = rows ``` What a hook returns ends the run : the API answers it. Return nothing to let the request go on. ## Schedulers and event listeners **Scheduler : remove old logs every night** ```python import datetime async def main(g): before = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=30)).isoformat() removed = await g.sys.db.remove_by_query('mongodb', 'shop', 'logs', find={'createdAt': {'$lt': before}}) g.logger.log('removed', removed.deleted_rows_count) ``` **Event listener : react to an order** ```python async def main(g): order = g.req.event_data or {} await g.sys.cache.set_key(f"order:{order.get('id')}", 'received', ttl_seconds=3600) ``` ## Utility classes A [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in Python is a module of its name : the other Python code of the account imports what it defines. **The utility class Tax** ```python from Round import cents # another utility class RATE = 0.2 def add(amount): return cents(amount * (1 + RATE)) ``` **A custom API which uses it** ```python from Tax import add async def main(g): return {"total": add(g.req.body.amount)} ``` - The name of the class is the name of the module : letters of a to z, digits and `_`, no word of Python, and one class of that name among the Python classes of the account (whatever its folder). - The module is run once per sandbox, when it is first imported; a class which is saved again is what the next request imports. - In the Git repository the class is `src/utility_classes/---/.py`, and the package `am` (what `from am import G` imports, with the types of every call) is in `am/python`. `pyproject.toml` lists those folders for Pyright / Pylance (`extraPaths`), pytest (`pythonpath`) and mypy (`mypy_path`); to run the code yourself, put them on `PYTHONPATH`. ## Test cases A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) written in Python has a function per test : every function whose name starts with `test_`, plain or `async`. What it raises is its failure. **A test case** ```python async def test_the_cost_of_a_parcel(g): await g.sys.test.mock("db.getAll", when={"collection": "rates"}, returns=[{"per_kg": 2.5}]) out = await g.sys.test.run_custom_api("shippingCost", body={"weight": 4}) assert out == {"cost": 10}, out assert len(await g.sys.test.calls("db.getAll")) == 1 ``` - `g.sys.test` : `run_custom_api(name, version=…, body=…, query=…, params=…, headers=…)`, `run_custom_api_full(…)` for the whole answer (`data`, `status_code`, `headers`, `logs`), `mock(method, when=…, returns=…)`, `mocks([Mock(…), …])`, `clear_mocks(method=None)`, `calls(method=None)`. - The custom API under test can be written in any language. The mocks of the Mocks tab of the test case answer too. ## Packages Add the package in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages) (`requests` `2.32.3`), then import it : **A package of PyPI** ```python import requests def main(g): answer = requests.get('https://api.example.com/rates', timeout=5) return answer.json() ``` - Versions follow PEP 440 : `2.32.3` is that version, `>=2,<3` and `~=2.32` are constraints. - A package which needs a system library (`psycopg` needs `libpq5`) gets it in the [Dockerfile](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#dockerfile), above `{{AM_PACKAGES}}`. ## How it runs - One runner process serves many requests at once : coroutines share the event loop, functions run on threads (64 at most per runner). CPU heavy code is better in a function; the sandbox opens more runners while all are busy. - The module of your code is compiled once and kept while it does not change : variables at module level live as long as the runner. - Values are plain JSON both ways : `datetime` becomes ISO text, `Decimal` a number, `set` and `tuple` lists, `bytes` base64, `NaN` and infinities `null`. What JSON can not hold (an open file) is an error which names its type. ## In the code editor The editor completes `g` as Python writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor). **Where the editor helps** ```python from am import G async def main(g: G): # g. → req, res, sys, logger, shared, Errors g.sys.db. → get_all, get_by_id, query, gen, full… g.logger.log(g.req.body.name) # g.req. → body, query, headers, auth… g.res. → status_code, headers… rows = await g.sys.db.get_all('mysql_8', 'inventory', 'customers', limit=10) # get_all(' → your instances, then its databases, then their tables , l → limit=, find=, sort=… for row in rows: g.logger.log(row.first_name, row['last_name']) # row. and row[' → the columns of customers return [row.first_name for row in rows] ``` - Inside the brackets of a call the editor offers its named arguments, each with its type and what it is for (`limit=`, `find=`, `select=`…) ; inside `find={'` it offers the columns of the table, inside `select='` and `sort='` too. The same for the classes of what a call takes : `GetAll(`. - What a call answers has its type : the columns are offered after `rows[0].`, `row.`, `row['` and `row.get('`, in a `for` loop and in a list made from the rows ; `changed.` (an `update_many`) offers `updated_rows_count`, `saved.` offers `row`, `rows` and `is_list`, the whole answer of `g.sys.db.full` offers `success`, `data`, `errors`… - The table is the one the call names : what it is given first (`get_all('mysql_8', 'inventory', 'customers')`), named arguments (`get_by_id(instance='mysql_8', ...)`), a `GetAll(…)` kept in a variable, or a `dict` as the API takes it. - `g.req.` and `g.res.` offer a property for each key of the request and of the answer, `g.req.auth.` the opened tokens. ## Reference | `g.` | Python | |---|---| | `g.req` | `am.Request` : `body`, `query`, `params`, `headers`, `auth`, `req_info`, `event_data`, `is_api_request_from_user` ; a `dict` with attribute access (`am.Obj`) too | | `g.res` | `am.Response` : `status_code`, `content_type`, `headers`, `shared`, `output`, `errors`, `warnings` ; an `am.Obj` too | | `g.sys.db` | `get_all`, `get_by_id`, `query`, `save_single_or_multiple`, `master_save`, `array_operations`, `update_by_id`, `update_many`, `replace_by_id`, `remove_by_id`, `remove_by_query`, `aggregate`, `count`, `distinct`, `distinct_query`, `get_all_by_stream`, `query_by_stream` | | `g.sys.db.gen` | the same with `_gen` : `get_all_gen`… | | `g.sys.db.full`, `g.sys.db.gen.full`, `g.sys.system.full`, `g.sys.cache.full` | the same calls, each with its whole answer : `ApiResponse` (`success`, `status_code`, `data`, `total_count`, `errors`, `warnings`) | | `g.sys.system` | `encrypt`, `encrypt_with`, `decrypt`, `decrypt_with`, `hash`, `get_secret`, `get_secrets`, `call_external_api`, `call_external_apis`, `get_token`, `get_tokens`, `execute_query`, `get_table_meta`, `get_table_metas`, `create_indexes`, `drop_indexes`, `drop_indexes_of_many`, `get_indexes`, `get_indexes_of_many`, `emit_event`, `emit_event_ws`, `is_valid_data_for_table(s)`, `is_valid_data_for_custom_api(s)`, `multi_tenant_instance(s)_updated`, `is_valid_connection_string(s)` | | `g.sys.cache` | `get_key`, `get_keys`, `set_key`, `set_keys`, `remove_key`, `remove_keys`, `reset_cache_db`, `reset_cache_dbs`, `reset_cache_custom_apis`, `reset_cache_custom_apis_of_tenant`, `reset_cache_system_apis`, `reset_cache_system_apis_of_tenant` | | `g.sys.call('db.getAll', …)` | any API by its name in JavaScript, with its arguments as they are | | what a call takes | `am.GetAll`, `am.GetById`, `am.Save`, `am.Query`, `am.DeepItem`, `am.GetToken`, `am.SetKey`, `am.Mock`… : made with the same named arguments | | `g.logger` | `log`, `debug`, `info`, `warn`, `warning`, `error` | | `g.shared` | `dict` of the runner | | `am.Errors`, `g.Errors` | `Errors(message, code=500)` or `Errors([{...}])`; `.code`, `.message`, `.errors` (the list) | ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) · [g.sys.db examples](https://docs.apimaker.dev/v4/examples/sys/db/getAll.html) --- # PHP > Write custom APIs, hooks, schedulers and event listeners of API Maker in PHP 8.3 or later. function main(AmG $g), $g->req, $g->res, the typed calls of $g->sys->db with named arguments, errors, Composer packages from Packagist, with many examples. Source: https://docs.apimaker.dev/v4/docs/languages/php.html Code in PHP defines `function main($g)`. It runs on the official image of PHP (`php:8.4-cli` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) name another version, 8.3 at least), with only the packages you add from Packagist. **hello.php** ```php $g->req->body->name ?? 'world']; } ``` | | | |---|---| | Runtime | PHP 8.3 or later : the released versions of `php:-cli` (`8.5`, `8.4`, `8.4.12`), no release candidate | | FFI extension | the runner loads API Maker with it : the image of the sandboxes gets it when it is built, and is not built without it | | `main` | `function main(AmG $g)` (the type is optional : `function main($g)` runs as well), or code which returns a function | | Answer | what `main` returns; `null` lets the API go on | | Calls of `$g->sys` | typed : named arguments, each with its type (`getAll('mysql_8', 'inventory', 'customers', limit: 10)`), and what a call answers has its type (a list of `AmObject`, `?AmObject`, `int`, `UpdateManyResult`) | | Values | objects of JSON are `AmObject`s : `$x->name` and `$x['name']` are the same, a key which is not there is `null` | | Packages | Packagist through Composer : `guzzlehttp/guzzle` `^7.9` | | Printing | `echo`, `print_r`, `var_dump` go to the console of the account | ## The request and the answer **Read the request, shape the answer** ```php req->query->status ?? 'open'; $signed = isset($g->req->headers['x-am-authorization']); $g->res->statusCode = 201; $g->res->headers['X-Request-Kind'] = 'orders'; return ['customer' => $g->req->params->id, 'status' => $status, 'signed' => $signed]; } ``` - `$g->req` (an `AmRequest`) and `$g->res` (an `AmResponse`) are `AmObject`s whose keys are documented properties, so an editor of PHP completes them : read and change them with `->` or `[]`, `foreach` over them, `->toArray()` for plain arrays. - What you change of `$g->req` in a pre hook is what the API sees. ### Bytes as the answer A file, a picture, a PDF : return its bytes in an `AmBytes`, or put that in `$g->res->output`, and name their kind of content. A text of PHP holds bytes as well as letters : `AmBytes` says that these are bytes. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. **A picture as the answer** ```php res->contentType = 'image/gif'; $g->res->headers['Content-Disposition'] = 'inline; filename="pixel.gif"'; return new AmBytes($picture); } ``` ## Calling API Maker : `$g->sys` Each method of `$g->sys->db`, `$g->sys->system` and `$g->sys->cache` is a call of the server, with its types. It takes named arguments, each with its type : what a call is about comes first and needs no name (the instance, the database, the table), the rest is given by name. What it answers has its type : rows are a list of `AmObject`, a row an `AmObject` or `null`, a count an `int`, what an API describes a class whose keys are documented properties (`$changed->updatedRowsCount`). A name no call has is an error of PHP (`Unknown named parameter`) : a key is never sent under a wrong name. **Database calls** ```php sys->db->getAll('mysql_8', 'inventory', 'customers', limit: 10, sort: '-customer_id'); $two = $g->sys->db->getById('mysql_8', 'inventory', 'customers', 2); $count = $g->sys->db->count('mysql_8', 'inventory', 'customers', find: ['country' => 'IN']); return ['first' => $rows[0] ?? null, 'two' => $two, 'inIndia' => $count]; } ``` **Save, update, remove** ```php sys->db->saveSingleOrMultiple(...$table, saveData: ['first_name' => 'Ada', 'last_name' => 'Lovelace']); $g->sys->db->updateById('mysql_8', 'inventory', 'customers', 2, updateData: ['first_name' => 'MALLORY']); $g->sys->db->removeById('mysql_8', 'inventory', 'customers', 101); return $saved->row(); // what a save answers : $saved->row(), $saved->rows(), $saved->isList() } ``` Every request runs in a fiber : while one waits for the server, the other requests of the runner go on. ### Rows as they arrive **Sum a large table** ```php sys->db->getAllByStream(new GetAll('mysql_8', 'inventory', 'orders'), function (AmObject $row) use (&$total) { $total += $row->amount ?? 0; }); return ['amount' => $total]; } ``` ### System APIs and cache **Encrypt, hash, events and cache keys** ```php sys->system->encrypt(['card' => '4111']); $digest = $g->sys->system->hash('my text'); $g->sys->system->emitEvent('order_created', ['id' => 7], ['notify_shop']); $g->sys->cache->setKey('last_order', '7', ttlSeconds: 600); return ['sealed' => $sealed, 'digest' => $digest, 'last' => $g->sys->cache->getKey('last_order')]; } ``` ## Errors **Refuse a request** ```php req->body->quantity)) { throw new AmErrors("'quantity' is missing.", 400); } return ['ok' => true]; } ``` **Catch the errors of a call** ```php sys->db->removeById('mysql_8', 'inventory', 'customers', 404); } catch (AmErrors $refused) { $g->logger->warn('not removed :', $refused->getMessage()); return ['removed' => false, 'code' => $refused->getCode()]; // $refused->errors[0]->field… } return ['removed' => true]; } ``` - Any other exception answers `500` with its class, message and the lines of your code (`InvalidArgumentException: x is 1`, then `at orders.php(7)`). - A warning is an error of the code : `Warning: Undefined array key "qty"`. Notices and deprecations are not. ## Logs, printing and shared values **Logs and shared values** ```php logger->log('order', 7, 'paid'); // "order 7 paid" in the logs of the answer $g->logger->error(['step' => 'payment']); // arrays and objects as indented JSON echo "kept in the console of the account\n"; $g->shared->calls = ($g->shared->calls ?? 0) + 1; // lives as long as the runner return $g->shared->calls; } ``` ## Hooks **Pre hook : only my rows** ```php req->auth->authAMDB ?? null; $find = $g->req->query->find ?? new AmObject(); $find->owner_id = $person->id ?? null; $g->req->query->find = $find; } ``` **Post hook : hide a column** ```php res->output ?? []; foreach ($rows as $row) { unset($row->password); } $g->res->output = $rows; } ``` ## Your own functions and classes Each piece of code is compiled in a namespace of its own : functions and classes of the same name in two custom APIs (or in two versions of one) never meet. Classes of PHP and of your packages are found as usual, `catch (Exception $e)` included. **Helpers and a class** ```php amount * (1 + $rate), 2), $this->currency); } } function main($g) { $price = new Price(100.0, 'EUR'); return ['gross' => $price->withTax(0.2)->amount, 'at' => new DateTimeImmutable('2026-01-01T00:00:00Z')]; } ``` ## Utility classes What a [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in PHP declares (classes, functions, constants) is there for the other PHP code of the account, by its name. **The utility class Tax** ```php Tax::add($g->req->body->amount), 'rate' => TAX_RATE]; } ``` - A utility class has **no `namespace` of its own** : API Maker loads each class in a namespace it chooses and imports what the code names (`Tax`, `cents`, `TAX_RATE`), so a class which was saved again is loaded next to the one it was. Name things without a leading `\`. - The file of a class is run once per sandbox. - In the Git repository the class is `src/utility_classes/---/.php`, and `composer.json` loads those files (`autoload.files`). ## Test cases A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) written in PHP has a function per test : every function whose name starts with `test_`. What it throws is its failure. **A test case** ```php sys->test->mock('db.getAll', when: ['collection' => 'rates'], returns: [['per_kg' => 2.5]]); $out = $g->sys->test->runCustomApi('shippingCost', body: ['weight' => 4]); if ($out->cost != 10) { throw new Exception("the cost is {$out->cost}"); } } ``` - `$g->sys->test` : `runCustomApi($name, version: …, body: …, query: …, params: …, headers: …)`, `runCustomApiFull(…)` for the whole answer (`data`, `statusCode`, `headers`, `logs`), `mock($method, when: …, returns: …)`, `mocks([new Mock(…), …])`, `clearMocks($method = null)`, `calls($method = null)`. - A test case has no `namespace` of its own. The custom API under test can be written in any language. ## Packages Add `guzzlehttp/guzzle` in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages), then : **A package of Packagist** ```php 5]))->get('https://api.example.com/rates'); return json_decode((string) $answer->getBody(), true); } ``` - Composer and what it unpacks with come into the image only when you add a package. - Versions are constraints of Composer : `7.9.2`, `^7.9`, `~2.0 || ^3.0`. ## In the code editor The editor completes `$g` as PHP writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor). **Where the editor helps** ```php → req, res, sys, logger, shared $g->sys->db-> → getAll, getById, query, gen, full… $g->logger->log($g->req->body->name); // $g->req-> → body, query, headers, auth… $g->res-> → statusCode, headers… $rows = $g->sys->db->getAll('mysql_8', 'inventory', 'customers', limit: 10); // getAll(' → your instances, then its databases, then their tables , l → limit: , find: , sort: … foreach ($rows as $row) { $g->logger->log($row->first_name, $row['last_name']); // $row-> and $row[' → the columns of customers } } ``` - Inside the brackets of a call the editor offers its named arguments, each with its type and what it is for (`limit: `, `find: `, `select: `…) ; inside `find: ['` it offers the columns of the table, inside `select: '` and `sort: '` too. The same for the classes of what a call takes : `new GetAll(`. - What a call answers has its type : the columns are offered after `$rows[0]->`, `$row->`, `$row['` and `$row->get('`, in a `foreach` and in the function given to `getAllByStream` ; `$changed->` (an `updateMany`) offers `updatedRowsCount`, `$saved->` offers `row`, `rows` and `isList`, the whole answer of `$g->sys->db->full` offers `success`, `data`, `errors`… - The table is the one the call names : in the call, in a variable (`$table + [...]`), in named arguments (`getById(instance: 'mysql_8', ...)`) or in a function which returns the query. - `$g->req->` and `$g->res->` offer the keys of the request and of the answer, `$g->req->auth->` the opened tokens. ## Reference | `$g->` | PHP | |---|---| | `req`, `res`, `shared` | `AmRequest`, `AmResponse`, `AmObject` : each an `AmObject` (`->`, `[]`, `foreach`, `->get($key, $default)`, `->toArray()`) | | `sys->db` | `getAll`, `getById`, `query`, `saveSingleOrMultiple`, `masterSave`, `arrayOperations`, `updateById`, `updateMany`, `replaceById`, `removeById`, `removeByQuery`, `aggregate`, `count`, `distinct`, `distinctQuery`, `getAllByStream`, `queryByStream`; `sys->db->gen->getAllGen`… | | `sys->db->full`, `sys->db->gen->full`, `sys->system->full`, `sys->cache->full` | the same calls, each with its whole answer : `ApiResponse` (`success`, `statusCode`, `data`, `totalCount`, `errors`, `warnings`) | | `sys->system` | `encrypt`, `encryptWith`, `decrypt`, `decryptWith`, `hash`, `getSecret`, `getSecrets`, `callExternalApi`, `callExternalApis`, `getToken`, `getTokens`, `executeQuery`, `getTableMeta`, `getTableMetas`, `createIndexes`, `dropIndexes`, `dropIndexesOfMany`, `getIndexes`, `getIndexesOfMany`, `emitEvent`, `emitEventWS`, `isValidDataForTable(s)`, `isValidDataForCustomAPI(s)`, `multiTenantInstance(s)Updated`, `isValidConnectionString(s)` | | `sys->cache` | `getKey`, `getKeys`, `setKey`, `setKeys`, `removeKey`, `removeKeys`, `resetCacheDB`, `resetCacheDBs`, `resetCacheCustomApis`, `resetCacheCustomApisOfTenant`, `resetCacheSystemApis`, `resetCacheSystemApisOfTenant` | | `sys->call('db.getAll', …)` | any API by its name in JavaScript, with its arguments as they are | | what a call takes | `GetAll`, `GetById`, `Save`, `Query`, `DeepItem`, `GetToken`, `SetKey`, `Mock`… : classes made with the same named arguments (`new GetAll('mysql_8', 'inventory', 'customers', limit: 10)`), given in place of them and to the calls of several | | `logger` | `log`, `debug`, `info`, `warn`, `error` | | errors | `throw new AmErrors($message, $code)` or `new AmErrors([[...], ...])`; `->getCode()`, `->getMessage()`, `->errors` (the list, each a `ResponseError`) | Method names may be written with underscores too (`get_by_id`). ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) --- # C# > Write custom APIs, hooks, schedulers and event listeners of API Maker in C# on .NET. public static async Task Main(G g), g.Req, g.Res, the typed calls of g.Sys.Db (new GetAll(…), List, GetAll), AmErrors, NuGet packages, with many examples. Source: https://docs.apimaker.dev/v4/docs/languages/csharp.html Code in C# is a class with `public static async Task Main(G g)`. It is compiled once, in memory, by the compiler of the .NET SDK, and runs on the SDK image (`mcr.microsoft.com/dotnet/sdk:10.0` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) name another version), with only the packages you add from NuGet. **Hello.cs** ```csharp file static class Api { public static async Task Main(G g) { return new { hello = g.Req.Body.GetString("name", "world") }; } } ``` | | | |---|---| | Runtime | .NET 8 or later, a version of `mcr.microsoft.com/dotnet/sdk` | | `Main` | a static method `Main(G g)` of any class : `Task`, `Task`, `object?` or `void` | | `file` | `file static class Api` : the class is known in its file only, so every piece of code can have its own `Api` and [git](https://docs.apimaker.dev/v4/docs/Git/git.html#project-of-every-language) still builds them as one project. `public static class Api` runs as well | | Answer | what `Main` returns, written as JSON; `null` lets the API go on | | Calls of `g.Sys` | typed : what a call takes is a class with a property for each key (`new GetAll("mysql_8", "inventory", "customers") { Limit = 10 }`), what it answers has its type (`List`, `JsonObject?`, `long`, `UpdateManyResult`, or your own class : `GetAll`) | | Values | `System.Text.Json.Nodes` : `JsonObject`, `JsonArray`, `JsonNode`, read by their types too (`row.GetString("name")`, `row.GetInt64("id")`, `row.To()`) | | Usings | `System`, `System.Collections.Generic`, `System.Linq`, `System.Threading.Tasks`, `System.Text.Json`, `System.Text.Json.Nodes` and `Am` are there already | | Packages | NuGet : `Humanizer.Core` `2.14.1` | | Printing | `Console.WriteLine` goes to the console of the account | ## The request and the answer **Read the request, shape the answer** ```csharp file static class Api { public static async Task Main(G g) { var status = g.Req.Query.GetString("status", "open"); var signed = g.Req.Headers["x-am-authorization"] is not null; g.Res.StatusCode = 201; g.Res.Headers["X-Request-Kind"] = "orders"; return new { customer = g.Req.Params["id"], status, signed }; } } ``` - An anonymous object, a record, a list or a `JsonNode` : anything `System.Text.Json` writes is an answer. A `byte[]` is bytes : see below. `NaN` and infinities are `null`, a `DateTime` in UTC is `2026-01-02T03:04:05.678Z`. - What you change of `g.Req` in a pre hook is what the API sees. - A body which is a list (`[{…}, {…}]`, several rows to save) is read with `g.Req["body"] as JsonArray` ; `g.Req.Body` is an empty `JsonObject` then. ### Bytes as the answer A file, a picture, a PDF : return its bytes (a `byte[]`, a `Memory`), or put `Json.Bytes(bytes)` in `g.Res.Output`, and name their kind of content. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. (A text under a kind of bytes, `image/gif`, is read as base64.) **A picture as the answer** ```csharp file static class Api { public static async Task Main(G g) { var picture = Convert.FromBase64String("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA="); g.Res.ContentType = "image/gif"; g.Res.Headers["Content-Disposition"] = "inline; filename=\"pixel.gif\""; return picture; } } ``` ## Calling API Maker : `g.Sys` Each method is a call of the server, with its types. What it takes is a class with a property for each of its keys, made with what it is about (`new GetAll("mysql_8", "inventory", "customers") { Limit = 10 }`) ; what it answers is typed too : rows are a `List`, a row a `JsonObject?`, a count a `long`, what an API describes a class with a property for each key (`changed.UpdatedRowsCount`). **Database calls** ```csharp file static class Api { public static async Task Main(G g) { var rows = await g.Sys.Db.GetAll(new GetAll("mysql_8", "inventory", "customers") { Limit = 10, Sort = "-customer_id" }); var two = await g.Sys.Db.GetById(new GetById("mysql_8", "inventory", "customers", 2)); var count = await g.Sys.Db.Count(new Count("mysql_8", "inventory", "customers") { Find = new { country = "IN" } }); return new { first = rows.FirstOrDefault(), name = two.GetString("first_name"), inIndia = count }; } } ``` - Rows as a class of yours (a record whose properties are the columns) : `List customers = await g.Sys.Db.GetAll(new GetAll(…))`, `await g.Sys.Db.GetById(…)`, or `row.To()` on any row. - A key the class has no property for is set by its name : `new GetAll("mysql_8", "inventory", "customers") { ["cursor"] = "abc" }`. - What a save answers : `saved.Row`, `saved.Rows`, `saved.IsList`. The generated APIs are under `g.Sys.Db.Gen`, the whole answer of a call under `Full` : `await g.Sys.Db.Full.GetAll(…)` gives an `ApiResponse>` (`Success`, `StatusCode`, `Data`, `TotalCount`, `Errors`). Calls at the same time are tasks : **Calls at the same time** ```csharp file static class Api { public static async Task Main(G g) { var orders = g.Sys.Db.Count(new Count("mongodb", "shop", "orders")); var products = g.Sys.Db.Count(new Count("mongodb", "shop", "products")); await Task.WhenAll(orders, products); return new { orders = orders.Result, products = products.Result }; } } ``` ### Rows as they arrive **Sum a large table** ```csharp file static class Api { public static async Task Main(G g) { var total = 0.0; await g.Sys.Db.GetAllByStream(new GetAll("mysql_8", "inventory", "orders"), row => total += row.GetDouble("amount")); return new { amount = total }; } } ``` The function of the rows can be `Action` or `Func`. ### System APIs and cache **Encrypt, hash, events and cache keys** ```csharp file static class Api { public static async Task Main(G g) { var sealedData = await g.Sys.System.Encrypt(new { card = "4111" }); var digest = await g.Sys.System.Hash("my text"); await g.Sys.System.EmitEvent("order_created", new { id = 7 }, new[] { "notify_shop" }); await g.Sys.Cache.SetKey("last_order", "7", 600); return new { sealedData, digest, last = await g.Sys.Cache.GetKey("last_order") }; } } ``` ## Errors **Refuse a request** ```csharp file static class Api { public static async Task Main(G g) { if (g.Req.Body["quantity"] is null) throw new AmErrors("'quantity' is missing.", 400); return new { ok = true }; } } ``` **Catch the errors of a call** ```csharp file static class Api { public static async Task Main(G g) { try { await g.Sys.Db.RemoveById(new RemoveById("mysql_8", "inventory", "customers", 404)); return new { removed = true }; } catch (AmErrors refused) { g.Logger.Warn("not removed :", refused.Message); return new { removed = false, code = refused.Code }; } } } ``` Any other exception answers `500` with its type and message, the message of what it happened because of (a type initializer which failed), and the lines of your code : `at Api.Helper(Int32 x) in orders.cs:line 10`. ## Logs, printing and shared values **Logs and shared values** ```csharp file static class Api { public static async Task Main(G g) { g.Logger.Log("order", 7, "paid"); // "order 7 paid" g.Logger.Error(new { step = "payment" }); // objects as indented JSON Console.WriteLine("kept in the console of the account"); // Lives as long as the runner, shared by every request it serves. return g.Shared.AddOrUpdate("calls", 1, (_, old) => (int)old! + 1); } } ``` ## Hooks **Pre hook : only my rows** ```csharp file static class Api { public static async Task Main(G g) { var query = g.Req.Query; if (query["find"] is not JsonObject find) query["find"] = find = new JsonObject(); find["owner_id"] = g.Req.Auth.GetObject("authAMDB")["id"]?.DeepClone(); return null; } } ``` **Post hook : hide a column** ```csharp file static class Api { public static async Task Main(G g) { if (g.Res.Output is JsonArray rows) foreach (var row in rows.OfType()) row.Remove("password"); return null; } } ``` ## Utility classes A [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in C# is compiled with the other C# code of the account, which names what it declares. **The utility class Tax** ```csharp public static class Tax { public const double Rate = 0.2; public static double Add(double amount) => Round.Cents(amount * (1 + Rate)); // Round : another utility class } ``` **A custom API which uses it** ```csharp file static class Api { public static async Task Main(G g) { return new { total = Tax.Add(g.Req.Body.GetDouble("amount")) }; } } ``` - Every class of the account is compiled with every piece of code which runs : `internal` and `public` types are both there, and what a class keeps in a static field is kept per piece of code. - A `Main(G g)` of a utility class is never taken for the `Main` of the code. - In the Git repository the class is `src/utility_classes/---/.cs` : `ApiMaker.csproj` compiles it with everything else. ## Test cases A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) written in C# has a method per test : every static method whose name starts with `Test` and which takes `G g`, in any class. What it throws is its failure. **A test case** ```csharp public static class ShippingTests { public static async Task TestTheCostOfAParcel(G g) { await g.Sys.Test.Mock(new Mock("db.getAll") { When = new { collection = "rates" }, Return = new[] { new { per_kg = 2.5 } } }); var output = await g.Sys.Test.RunCustomApi(new RunCustomApi("shippingCost") { Body = new { weight = 4 } }); if (output.GetDouble("cost") != 10) throw new Exception("the cost is " + output?["cost"]); } } ``` - `g.Sys.Test` : `RunCustomApi(new RunCustomApi(…))`, `RunCustomApiFull(…)` for the whole answer (`Data`, `StatusCode`, `Headers`, `Logs`), `Mock(new Mock(…))`, `Mocks(…)`, `ClearMocks(method)`, `Calls(method)`. - The tests run in the order they are written. The custom API under test can be written in any language. ## Packages Add `Humanizer.Core` in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages), then : **A package of NuGet** ```csharp using Humanizer; file static class Api { public static object? Main(G g) => new { words = 3.ToWords() }; } ``` - Versions are NuGet versions : `13.0.3`, floating `13.*`, ranges `[1.0,2.0)`. - The packages are restored with the runner when the image is built; the compiler sees them at once. ## How it runs - The code is compiled once per version, in memory, with its symbols : stack traces say its lines. The first save of code starts that compilation in the background, so the first request does not wait for it. - Every request is a task of the runner; requests run at the same time. A thread of the pool which has nothing left to do waits at once, it does not spin first (`DOTNET_ThreadPool_UnfairSemaphoreSpinLimit=0`) : a request which arrives by itself then costs less than half the processor time. A variable of that name in the environment of your sandboxes is kept as you set it. - `nullable` is on : the compiler warns about `null`, it does not refuse. - A `JsonNode` belongs to one parent : to put a node of the request into another object, `DeepClone()` it. ## In the code editor The editor completes `g` as C# writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor). **Where the editor helps** ```csharp file static class Api { public static async Task Main(G g) { // g. → Req, Res, Sys, Logger, Shared g.Sys.Db. → GetAll, GetById, Query, Gen, Full… g.Logger.Log(g.Req.Body.GetString("name")); // g.Req. → Body, Query, Headers, Auth… g.Res. → StatusCode, Headers… var rows = await g.Sys.Db.GetAll(new GetAll("mysql_8", "inventory", "customers") { Limit = 10 }); // new GetAll(" → your instances, then its databases, then their tables { L → Limit = , Find = , Sort = … foreach (var row in rows) g.Logger.Log(row.GetString("first_name"), row["last_name"]); // row.GetString(" and row[" → the columns of customers return rows[0]["first_name"]; } } ``` - What a call answers has its type : on the rows of a `GetAll` the columns are offered inside the quotes of `GetString`, `GetInt64`, `GetDouble`, `GetObject`… and of an index (`rows[0]["`, `row["`), in a `foreach`, in a lambda (`rows.Select(row => row.GetString("…"))`) and in the function given to `GetAllByStream`. `changed.` (an `UpdateMany`) offers `UpdatedRowsCount`, `saved.` offers `Row`, `Rows` and `IsList`, the whole answer of `g.Sys.Db.Full` offers `Success`, `Data`, `Errors`… - Between the braces of what a call takes the editor offers its properties as names, each with what it is for : after `new GetAll("mysql_8", "inventory", "customers") { ` it offers `Limit`, `Find`, `Sort`, `Select`… Inside `Find = new { ` it offers the columns of the table, inside `Select = "` and `Sort = "` too. - `g.Req.` and `g.Res.` offer a property for each key of the request and of the answer ; `g.Req.Auth.` the opened tokens, written as C# reads a key (`g.Req.Auth["authAMUser"]`, and `?["name"]` after `?.`). - The columns follow a row the same way : `rows[0].` then `first_name` writes `rows[0]["first_name"]`. ## Reference | `g.` | C# | |---|---| | `Req` | `Body`, `Query`, `Params`, `Headers`, `Auth`, `ReqInfo` (each a `JsonObject`), `EventData`, `IsApiRequestFromUser` | | `Res` | `StatusCode`, `ContentType`, `Headers`, `Shared`, `Output`, `Errors`, `Warnings` | | `Sys.Db` | `GetAll(GetAll)`, `GetById(GetById)`, `Query(Query)`, `SaveSingleOrMultiple(Save)`, `MasterSave(Save)`, `ArrayOperations(ArrayOperations)`, `UpdateById(UpdateById)`, `UpdateMany(UpdateMany)`, `ReplaceById(UpdateById)`, `RemoveById(RemoveById)`, `RemoveByQuery(RemoveByQuery)`, `Aggregate(Aggregate)`, `Count(Count)`, `Distinct(Distinct)`, `DistinctQuery(Distinct)`, `GetAllByStream`, `QueryByStream` ; `GetAll`, `GetById`… for rows as your own class ; `Sys.Db.Gen.GetAllGen`… | | `Sys.Db.Full`, `Sys.Db.Gen.Full`, `Sys.System.Full`, `Sys.Cache.Full` | the same calls, each with its whole answer : `ApiResponse` (`Success`, `StatusCode`, `Data`, `TotalCount`, `Errors`, `Warnings`) | | `Sys.System` | `Encrypt`, `EncryptWith`, `Decrypt`, `DecryptWith`, `Hash`, `GetSecret`, `GetSecrets`, `CallExternalApi`, `CallExternalApis`, `GetToken`, `GetTokens`, `ExecuteQuery`, `GetTableMeta`, `GetTableMetas`, `CreateIndexes`, `DropIndexes`, `DropIndexesOfMany`, `GetIndexes`, `GetIndexesOfMany`, `EmitEvent`, `EmitEventWS`, `IsValidDataForTable(s)`, `IsValidDataForCustomAPI(s)`, `MultiTenantInstance(s)Updated`, `IsValidConnectionString(s)` | | `Sys.Cache` | `GetKey`, `GetKeys`, `SetKey`, `SetKeys`, `RemoveKey`, `RemoveKeys`, `ResetCacheDB`, `ResetCacheDBs`, `ResetCacheCustomApis`, `ResetCacheCustomApisOfTenant`, `ResetCacheSystemApis`, `ResetCacheSystemApisOfTenant` | | `Sys.Call("db.getAll", …)` | any API by its name in JavaScript, with its arguments as they are | | a `JsonObject`, a `JsonNode` | `GetString`, `GetInt64`, `GetInt32`, `GetDouble`, `GetBoolean`, `GetObject`, `GetArray`, `GetObjects`, `GetStrings`, `To()` | | `Logger` | `Log`, `Debug`, `Info`, `Warn`, `Error` | | `Shared` | `ConcurrentDictionary` of the runner | | errors | `throw new AmErrors(message, code)` or `new AmErrors(JsonArray)`; `.Errors` (a `List`), `.Code` | ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) --- # Go > Write custom APIs, hooks, schedulers and event listeners of API Maker in Go. func Main(g *am.G) (any, error), g.Req, g.Res, the typed calls of g.Sys.Db (am.GetAll, []am.M), am.Fail, Go modules, with many examples. Source: https://docs.apimaker.dev/v4/docs/languages/go.html Code in Go defines `func Main(g *am.G) (any, error)` in a package which imports `apimaker/am`. It is built once as a plugin of the runner, on the official image of Go (`golang:1.27` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) name another version), with only the modules you add. **hello.go** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { name := g.Req.Body().String("name") if name == "" { name = "world" } return am.M{"hello": name}, nil } ``` | | | |---|---| | Runtime | Go 1.21 or later, a version of `golang:` | | `Main` | `func Main(g *am.G) (any, error)` : what it returns is the answer, an error refuses | | Calls of `g.Sys` | typed : what a call takes is a struct (`am.GetAll{…}`), what it answers has its type (`[]am.M`, `int`, `am.UpdateManyResult`) | | Values | `am.M` (`map[string]any`) for objects and rows, `[]any` for lists, `float64` for numbers | | Package | any name, `package code` in a new item : the sandbox builds it as a plugin whatever it is named. In [git](https://docs.apimaker.dev/v4/docs/Git/git.html#project-of-every-language) every piece of code is a package of its own, in a folder of its own | | Packages | Go modules : `github.com/google/uuid` `v1.6.0` | | Printing | `fmt.Println` and `log` go to the console of the account | ## The request and the answer `g.Req` and `g.Res` have a method for each key they are known to have : `g.Req.Body()`, `g.Req.Query()`, `g.Req.Params()`, `g.Req.Headers()`, `g.Res.SetStatusCode(201)`. What they give is an `am.M` : `Map`, `List`, `String`, `Float`, `Int` and `Bool` read a key without a type assertion and give the zero value when it is not there. The map is the one inside the request, so changing it changes the request. A body which is a list (`[{…}, {…}]`, several rows to save) is read with `g.Req.List("body")` ; `g.Req.Body()` is an empty `am.M` then. **Read the request, shape the answer** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { status := g.Req.Query().String("status") if status == "" { status = "open" } _, signed := g.Req.Headers()["x-am-authorization"] g.Res.SetStatusCode(201) g.Res.Headers()["X-Request-Kind"] = "orders" return am.M{"customer": g.Req.Params().String("id"), "status": status, "signed": signed}, nil } ``` ### Bytes as the answer A file, a picture, a PDF : return its bytes (a `[]byte`), or give them to `g.Res.SetOutput`, and name their kind of content. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. (A text under a kind of bytes, `image/gif`, is read as base64.) **A picture as the answer** ```go package code import ( "encoding/base64" "apimaker/am" ) func Main(g *am.G) (any, error) { picture, err := base64.StdEncoding.DecodeString("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=") if err != nil { return nil, err } g.Res.SetContentType("image/gif") g.Res.Headers()["Content-Disposition"] = `inline; filename="pixel.gif"` return picture, nil } ``` ## Calling API Maker : `g.Sys` Every call has its types. What it takes is a struct of `am` whose fields are its keys (`am.GetAll`, `am.GetById`, `am.Count`…) ; what it answers is typed too : rows are a `[]am.M`, a row an `am.M` (`nil` when there is none), a count an `int`, what an API describes a struct (`am.UpdateManyResult`). **Database calls** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { rows, err := g.Sys.Db.GetAll(am.GetAll{Instance: "mysql_8", Database: "inventory", Collection: "customers", Limit: 10, Sort: "-customer_id"}) if err != nil { return nil, err } two, err := g.Sys.Db.GetById(am.GetById{Instance: "mysql_8", Database: "inventory", Collection: "customers", Id: 2}) if err != nil { return nil, err } count, err := g.Sys.Db.Count(am.Count{Instance: "mysql_8", Database: "inventory", Collection: "customers", Find: am.M{"country": "IN"}}) if err != nil { return nil, err } var first am.M if len(rows) > 0 { first = rows[0] } return am.M{"first": first, "name": two.String("first_name"), "inIndia": count}, nil } ``` - A key which is left out is not sent. A flag which is on by default is a pointer, so `false` can be said : `NoCycle: am.Ptr(false)`. - A key the struct has no field for goes in `Other` : `am.GetAll{Instance: "mysql_8", Other: am.M{"cursor": "abc"}}`. - A row as a struct of your own : `customer, err := am.To[Customer](rows[0])`. **What a call answers has its type** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { changed, err := g.Sys.Db.UpdateMany(am.UpdateMany{Instance: "mysql_8", Database: "inventory", Collection: "customers", Find: am.M{"country": "IN"}, UpdateData: am.M{"active": true}}) if err != nil { return nil, err } // What a save answers : saved.Row (one row was saved), saved.Rows (a list was), saved.IsList saved, err := g.Sys.Db.SaveSingleOrMultiple(am.Save{Instance: "mysql_8", Database: "inventory", Collection: "customers", SaveData: am.M{"first_name": "Ada"}}) if err != nil { return nil, err } // The whole answer of a call : whole.Success, whole.StatusCode, whole.Data ([]am.M), whole.TotalCount, whole.Errors whole, err := g.Sys.Db.Full.GetAll(am.GetAll{Instance: "mysql_8", Database: "inventory", Collection: "customers", GetTotalCount: true}) if err != nil { return nil, err } return am.M{"updated": changed.UpdatedRowsCount, "id": saved.Row["customer_id"], "of": whole.TotalCount, "rows": len(whole.Data)}, nil } ``` Calls at the same time are goroutines : **Calls at the same time** ```go package code import ( "sync" "apimaker/am" ) func Main(g *am.G) (any, error) { counts := map[string]any{} var lock sync.Mutex var wait sync.WaitGroup for _, collection := range []string{"orders", "products"} { wait.Add(1) go func(collection string) { defer wait.Done() count, _ := g.Sys.Db.Count(am.Count{Instance: "mongodb", Database: "shop", Collection: collection}) lock.Lock() counts[collection] = count lock.Unlock() }(collection) } wait.Wait() return counts, nil } ``` ### Rows as they arrive **Sum a large table** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { total := 0.0 err := g.Sys.Db.GetAllByStream(am.GetAll{Instance: "mysql_8", Database: "inventory", Collection: "orders"}, func(row am.M) error { total += row.Float("amount") return nil }) return am.M{"amount": total}, err } ``` ### System APIs and cache **Encrypt, hash, events and cache keys** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { sealed, err := g.Sys.System.Encrypt(am.M{"card": "4111"}) if err != nil { return nil, err } digest, _ := g.Sys.System.Hash("my text") _, _ = g.Sys.System.EmitEvent("order_created", am.M{"id": 7}, "notify_shop") _, _ = g.Sys.Cache.SetKey("last_order", "7", 600) last, _ := g.Sys.Cache.GetKey("last_order") return am.M{"sealed": sealed, "digest": digest, "last": last}, nil } ``` ## Errors Return `am.Fail(code, message)` to refuse, or an `*am.Errors` with several errors : **Refuse a request** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { if g.Req.Body().Get("quantity") == nil { return nil, am.Fail(400, "'quantity' is missing.") } return am.M{"ok": true}, nil } ``` A call which fails gives an `*am.Errors` with the errors of the server : **Catch the errors of a call** ```go package code import ( "errors" "apimaker/am" ) func Main(g *am.G) (any, error) { _, err := g.Sys.Db.RemoveById(am.RemoveById{Instance: "mysql_8", Database: "inventory", Collection: "customers", Id: 404}) var refused *am.Errors if errors.As(err, &refused) { g.Logger.Warn("not removed :", refused.Error()) return am.M{"removed": false, "code": refused.Code()}, nil } return am.M{"removed": err == nil}, err } ``` - Another error answers `500` with its message. A panic answers `500` with `panic: …` and the lines of your code (`at orders.go:14 in helper()`). - A panic in a goroutine you started ends the runner : recover inside your goroutines. ## Logs, printing and shared values **Logs and shared values** ```go package code import ( "fmt" "apimaker/am" ) func Main(g *am.G) (any, error) { g.Logger.Log("order", 7, "paid") // "order 7 paid" g.Logger.Error(am.M{"step": "payment"}) // maps, slices and structs as indented JSON fmt.Println("kept in the console of the account") // Lives as long as the runner : Update changes a value while no other request does. calls := g.Shared.Update("calls", func(old any) any { count, _ := old.(int) return count + 1 }) return calls, nil } ``` ## Hooks **Pre hook : only my rows** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { person := g.Req.Auth().Map("authAMDB") query := g.Req.Query() find := query.Map("find") if find == nil { find = am.M{} query["find"] = find } find["owner_id"] = person.Get("id") return nil, nil } ``` **Post hook : hide a column** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { for _, row := range g.Res.List("output") { delete(am.AsM(row), "password") } return nil, nil } ``` ## Utility classes A [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in Go is a package. The other Go code of the account imports it by the folder it has in the Git repository, which is the module `apimaker` : **The utility class Tax, in the folder utils** ```go package tax import round "apimaker/src/utility_classes/utils---Round" // another utility class var Rate = 0.2 func Add(amount float64) float64 { return round.Cents(amount * (1 + Rate)) } ``` **A custom API which uses it** ```go package code import ( "apimaker/am" tax "apimaker/src/utility_classes/utils---Tax" ) func Main(g *am.G) (any, error) { return am.M{"total": tax.Add(g.Req.Body().Float("amount"))}, nil } ``` - The folder of a class is `src/utility_classes/---` (`utils---Tax` for the class `Tax` in `utils/`) : letters of a to z, digits, `.`, `-` and `_`. The name after `package` is yours; name the import as you like. - The package is built once and shared by all Go code of the account in a sandbox : what it keeps in a variable is kept there. A class which is saved again is another build, which the code gets when it is compiled again on its next run. - `go build ./...` in the repository builds the same imports : nothing else is needed. ## Test cases A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) written in Go lists its tests in `Tests`, in the place of a `Main`. A test is a `func(g *am.G) error` : the error it returns, or what it panics with, is its failure. **A test case** ```go package code import ( "fmt" "apimaker/am" ) func theCostOfAParcel(g *am.G) error { if err := g.Sys.Test.Mock(am.Mock{Method: "db.getAll", When: am.M{"collection": "rates"}, Return: []any{am.M{"per_kg": 2.5}}}); err != nil { return err } out, err := g.Sys.Test.RunCustomApi(am.RunCustomApi{Name: "shippingCost", Body: am.M{"weight": 4}}) if err != nil { return err } if cost := am.AsM(out).Float("cost"); cost != 10 { return fmt.Errorf("the cost is %v", cost) } return nil } var Tests = []am.Test{ {Name: "the cost of a parcel", Run: theCostOfAParcel}, } ``` - `g.Sys.Test` : `RunCustomApi(am.RunCustomApi{…})`, `RunCustomApiFull(…)` (the whole answer : `Data`, `StatusCode`, `Headers`, `Logs`), `Mock(am.Mock{…})`, `Mocks([]am.Mock{…})`, `ClearMocks(method...)`, `Calls(method...)`. - The custom API under test can be written in any language. ## Packages Add `github.com/google/uuid` in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages), then : **A Go module** ```go package code import ( "github.com/google/uuid" "apimaker/am" ) func Main(g *am.G) (any, error) { return am.M{"id": uuid.NewString()}, nil } ``` - A version without `v` gets one : `1.6.0` is `v1.6.0`; empty is `latest`. - Modules are downloaded when the image is built; while code runs nothing is downloaded (`GOPROXY=off`). ## How it runs - Each piece of code is built once as a plugin of the runner (about a second), from the same module as the runner : the packages of the account and `apimaker/am` are shared. Saving starts the build in the background; code which does not compile is reported in the console of the account (`main.go:6:9: undefined: missing`). - Every request is a goroutine; requests run at the same time, on four threads of Go at most. Code of an API waits for its database most of the time, and Go wakes an idle thread for every goroutine which becomes ready : with a thread for every core of a big machine that costs more than the request. Code which computes on many cores sets `GOMAXPROCS` to the number it wants, with an `ENV` line of the [Dockerfile](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#dockerfile) of Go : `ENV GOMAXPROCS=16`. - Variables at package level live as long as the runner : protect them with a mutex, or use `g.Shared`. ## In the code editor The editor completes `g` as Go writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor). **Where the editor helps** ```go package code import "apimaker/am" func Main(g *am.G) (any, error) { // g. → Req, Res, Sys, Logger, Shared g.Sys.Db. → GetAll, GetById, Query, Gen, Full… g.Logger.Log(g.Req.Body()) // g.Req. → Body, Query, Headers, Auth… g.Res. → SetStatusCode, Headers… rows, err := g.Sys.Db.GetAll(am.GetAll{Instance: "mysql_8", Database: "inventory", Collection: "customers"}) // am.GetAll{ → Instance, Database, Collection, Find, Limit, Sort… // Instance: " → your instances Collection: " → the tables of mysql_8.inventory if err != nil { return nil, err } for _, row := range rows { g.Logger.Log(row.String("first_name")) // .String(" → the columns of customers } return rows, nil } ``` - `am.` offers what the package has : the structs of what the calls take (`am.GetAll`, `am.Save`…), `am.M`, `am.Fail`, `am.To`. Between the braces of such a struct the editor offers its fields, each with what it is for ; inside `Find: am.M{"` the columns of the table, inside `Select: "` and `Sort: "` too. - What a call answers has its type : on the rows of a `GetAll` the columns are offered inside the quotes of `Get`, `Map`, `List`, `String`, `Float`, `Int` and `Bool`, and of an index (`rows[0]["`) ; `changed.` offers `UpdatedRowsCount`, `saved.` offers `Row`, `Rows` and `IsList`, `whole.` (a call of `g.Sys.Db.Full`) `Success`, `Data`, `Errors`… - `g.Req.` and `g.Res.` offer a method for each key of the request and of the answer ; `g.Req.Auth().` the opened tokens. ## Reference | `g.` | Go | |---|---| | `Req` | `Body()`, `Query()`, `Params()`, `Headers()`, `Auth()`, `ReqInfo()` (each an `am.M`), `EventData()`, `IsApiRequestFromUser()`, and `Set…` for each | | `Res` | `SetStatusCode`, `SetContentType`, `Headers()`, `Shared()`, `Output()`, `SetOutput`, `Errors()`, `Warnings()` | | an `am.M` | `Get`, `Map`, `List`, `String`, `Float`, `Int`, `Bool` | | `Sys.Db` | `GetAll(am.GetAll)`, `GetById(am.GetById)`, `Query(am.Query)`, `SaveSingleOrMultiple(am.Save)`, `MasterSave(am.Save)`, `ArrayOperations(am.ArrayOperations)`, `UpdateById(am.UpdateById)`, `UpdateMany(am.UpdateMany)`, `ReplaceById(am.UpdateById)`, `RemoveById(am.RemoveById)`, `RemoveByQuery(am.RemoveByQuery)`, `Aggregate(am.Aggregate)`, `Count(am.Count)`, `Distinct(am.Distinct)`, `DistinctQuery(am.Distinct)`, `GetAllByStream`, `QueryByStream`; `Sys.Db.Gen.GetAllGen`… | | `Sys.Db.Full`, `Sys.Db.Gen.Full`, `Sys.System.Full`, `Sys.Cache.Full` | the same calls, each with its whole answer : `am.ApiResponse[T]` (`Success`, `StatusCode`, `Data`, `TotalCount`, `Errors`, `Warnings`) | | `Sys.System` | `Encrypt`, `EncryptWith`, `Decrypt`, `DecryptWith`, `Hash`, `GetSecret`, `GetSecrets`, `CallExternalApi`, `CallExternalApis`, `GetToken`, `GetTokens`, `ExecuteQuery`, `GetTableMeta`, `GetTableMetas`, `CreateIndexes`, `DropIndexes`, `DropIndexesOfMany`, `GetIndexes`, `GetIndexesOfMany`, `EmitEvent`, `EmitEventWS`, `IsValidDataForTable(s)`, `IsValidDataForCustomAPI(s)`, `MultiTenantInstance(s)Updated`, `IsValidConnectionString(s)` | | `Sys.Cache` | `GetKey`, `GetKeys`, `SetKey`, `SetKeys`, `RemoveKey`, `RemoveKeys`, `ResetCacheDB`, `ResetCacheDBs`, `ResetCacheCustomApis`, `ResetCacheCustomApisOfTenant`, `ResetCacheSystemApis`, `ResetCacheSystemApisOfTenant` | | `Sys.Call("db.getAll", …)` | any API by its name in JavaScript, with its arguments as they are | | `Logger` | `Log`, `Debug`, `Info`, `Warn`, `Error` | | `Shared` | `Get`, `Set`, `Update` | | errors | `am.Fail(code, message)`, `&am.Errors{List: []am.M{…}}` | ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) --- # Rust > Write custom APIs, hooks, schedulers and event listeners of API Maker in Rust. pub fn main(g: &mut G) -> Result, g.req, g.res, g.sys.db, am::fail, crates from crates.io, with many examples. Source: https://docs.apimaker.dev/v4/docs/languages/rust.html Code in Rust defines `pub fn main(g: &mut G) -> Result`. It is built once as a library the runner loads, on the official image of Rust (`rust:1-slim` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) name another version), with only the crates you add. **hello.rs** ```rust use am::{Errors, G, Value, json}; pub fn main(g: &mut G) -> Result { let name = g.req.body()["name"].as_str().unwrap_or("world"); Ok(json!({ "hello": name })) } ``` | | | |---|---| | Runtime | stable Rust, 1.88 or later : a version of `rust:-slim` (edition 2024) | | `main` | `pub fn main(g: &mut G) -> Result` : `Ok(value)` answers, `Value::Null` lets the API go on | | Calls of `g.sys` | typed : what a call takes is a type made key after key (`GetAll::new("mysql_8", "inventory", "customers").limit(10)`), what it answers has its type (`Vec`, `Option`, `i64`, `UpdateManyResult`) | | Values | `serde_json::Value` (`am::Value`, `am::json!`) : `g.req.body()["id"]` ; a row as a struct of yours with `.to()` | | `?` | works on every error : the calls of `g.sys`, parsing, `serde_json`, your own errors | | Crates | crates.io : `serde[derive]` `1` is `serde` with the feature `derive` | | Printing | `println!` and `eprintln!` go to the console of the account | ## The request and the answer `g.req` and `g.res` have a method for each key they are known to have : `g.req.body()`, `g.req.query()`, `g.req.params()`, `g.req.headers()` read (`…_mut()` to change one where it is), `g.res.set_status_code(201)` sets. Each gives a `Value` : reading a key which is not there gives `Value::Null`; writing one makes it. The request and the answer are values of JSON themselves too : `g.req["added"] = json!(1)`. A body which is a list (`[{…}, {…}]`, several rows to save) is that list : `g.req.body().as_array()`. **Read the request, shape the answer** ```rust use am::{Errors, G, Value, json}; pub fn main(g: &mut G) -> Result { let status = g.req.query()["status"].as_str().unwrap_or("open").to_owned(); let signed = !g.req.headers()["x-am-authorization"].is_null(); g.res.set_status_code(201); g.res.headers_mut()["X-Request-Kind"] = json!("orders"); Ok(json!({ "customer": g.req.params()["id"], "status": status, "signed": signed })) } ``` ### Bytes as the answer A file, a picture, a PDF : return `am::bytes(…)` of its bytes, or give that to `g.res.set_output`, and name their kind of content. A value of JSON has nothing which stands for bytes : `am::bytes` is the form they travel in. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. **A picture as the answer** ```rust use am::{Errors, G, Value, json}; pub fn main(g: &mut G) -> Result { // The bytes of a picture : read from a file, downloaded, or made by the code. let picture: &[u8] = b"GIF89a\x01\0\x01\0\x80\0\0\xff\xff\xff\xff\xff\xff!\xf9\x04\x01\n\0\x01\0,\0\0\0\0\x01\0\x01\0\0\x02\x02L\x01\0;\0"; g.res.set_content_type("image/gif"); g.res.headers_mut()["Content-Disposition"] = json!("inline; filename=\"pixel.gif\""); Ok(am::bytes(picture)) } ``` ## Calling API Maker : `g.sys` Each method is a call of the server, with its types : it waits for the answer and gives a `Result`. What it takes is a type of `am` made with what it is about, then key after key (`GetAll`, `GetById`, `Count`…) ; what it answers is typed too : rows are a `Vec`, a row an `Option`, a count an `i64`, what an API describes a struct (`UpdateManyResult`). **Database calls** ```rust use am::{Count, Errors, G, GetAll, GetById, Value, json}; pub fn main(g: &mut G) -> Result { let rows = g.sys.db.get_all(GetAll::new("mysql_8", "inventory", "customers").limit(10).sort("-customer_id"))?; let two = g.sys.db.get_by_id(GetById::new("mysql_8", "inventory", "customers", 2))?; let count = g.sys.db.count(Count::new("mysql_8", "inventory", "customers").find(json!({ "country": "IN" })))?; Ok(json!({ "first": rows.first(), "name": two.map(|row| row["first_name"].clone()), "in_india": count })) } ``` **Save, update, remove** ```rust use am::{Errors, G, RemoveById, Save, UpdateById, Value, json}; pub fn main(g: &mut G) -> Result { let saved = g.sys.db.save_single_or_multiple(Save::new("mysql_8", "inventory", "customers", json!({ "first_name": "Ada", "last_name": "Lovelace" })))?; g.sys.db.update_by_id(UpdateById::new("mysql_8", "inventory", "customers", 2).update_data(json!({ "first_name": "MALLORY" })))?; g.sys.db.remove_by_id(RemoveById::new("mysql_8", "inventory", "customers", 101))?; // What a save answers : `saved.row()`, `saved.rows()`, `saved.is_list()`. Ok(json!({ "saved": saved.row() })) } ``` - A key the type has no method for is set as it is : `GetAll::new("mysql_8", "inventory", "customers").with("cursor", "abc")`. A type is made of JSON too : `GetAll::from(json!({ … }))`. - A row as a struct of your own (`#[derive(serde::Deserialize)]`) : `let customers: Vec = rows.to()?;`. - The generated APIs are methods of `g.sys.db` too (`get_all_gen`…). The whole answer of a call (`success`, `status_code`, `data`, `total_count`, `errors`) : `g.sys.db.full.get_all(…)?`, an `ApiResponse>`. Calls at the same time are threads, with `std::thread::scope` : **Calls at the same time** ```rust use am::{Count, Errors, G, Value, json}; pub fn main(g: &mut G) -> Result { let db = &g.sys.db; let (orders, products) = std::thread::scope(|scope| { let orders = scope.spawn(|| db.count(Count::new("mongodb", "shop", "orders"))); let products = scope.spawn(|| db.count(Count::new("mongodb", "shop", "products"))); (orders.join(), products.join()) }); let orders = orders.unwrap_or_else(|_| Ok(0))?; let products = products.unwrap_or_else(|_| Ok(0))?; Ok(json!({ "orders": orders, "products": products })) } ``` ### Rows as they arrive **Sum a large table** ```rust use am::{Errors, G, GetAll, Value, json}; pub fn main(g: &mut G) -> Result { let mut total = 0.0; g.sys.db.get_all_by_stream(GetAll::new("mysql_8", "inventory", "orders"), |row| { total += row["amount"].as_f64().unwrap_or(0.0); Ok(()) })?; Ok(json!({ "amount": total })) } ``` The function stays on the thread of the request : it can borrow and change your variables. ### System APIs and cache **Encrypt, hash, events and cache keys** ```rust use am::{Errors, G, Value, json}; pub fn main(g: &mut G) -> Result { let sealed = g.sys.system.encrypt(json!({ "card": "4111" }))?; let digest = g.sys.system.hash("my text")?; g.sys.system.emit_event("order_created", json!({ "id": 7 }), &["notify_shop"])?; g.sys.cache.set_key("last_order", "7", Some(600))?; let last = g.sys.cache.get_key("last_order")?; Ok(json!({ "sealed": sealed, "digest": digest, "last": last })) } ``` ## Errors **Refuse a request** ```rust use am::{Errors, G, Value, json}; pub fn main(g: &mut G) -> Result { let Some(quantity) = g.req.body()["quantity"].as_u64() else { return Err(am::fail(400, "'quantity' is missing.")); }; Ok(json!({ "quantity": quantity })) } ``` **Catch the errors of a call** ```rust use am::{Errors, G, RemoveById, Value, json}; pub fn main(g: &mut G) -> Result { match g.sys.db.remove_by_id(RemoveById::new("mysql_8", "inventory", "customers", 404)) { Ok(_) => Ok(json!({ "removed": true })), Err(refused) => { g.logger.warn(format!("not removed : {}", refused.message())); Ok(json!({ "removed": false, "code": refused.code() })) } } } ``` - `?` on any error type answers `500` with its message : `let n: i64 = text.parse()?;` gives `invalid digit found in string`. - A panic answers `500` with `panic: …` and the lines of your code (`at main.rs:8 in helper`). ## Logs, printing and shared values **Logs and shared values** ```rust use am::{Errors, G, Value, json}; pub fn main(g: &mut G) -> Result { g.logger.log(format!("order {} paid", 7)); g.logger.error(json!({ "step": "payment" })); // values as indented JSON println!("kept in the console of the account"); // Shared by every piece of code of the sandbox : update changes a value while no other request does. let calls = g.shared.update("calls", |old| json!(old.and_then(|n| n.as_i64()).unwrap_or(0) + 1)); Ok(calls) } ``` ## Hooks **Pre hook : only my rows** ```rust use am::{Errors, G, Value}; pub fn main(g: &mut G) -> Result { let owner = g.req.auth()["authAMDB"]["id"].clone(); g.req.query_mut()["find"]["owner_id"] = owner; Ok(Value::Null) } ``` **Post hook : hide a column** ```rust use am::{Errors, G, Value}; pub fn main(g: &mut G) -> Result { if let Some(rows) = g.res.output_mut().as_array_mut() { for row in rows { if let Some(row) = row.as_object_mut() { row.remove("password"); } } } Ok(Value::Null) } ``` ## Utility classes A [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in Rust is a module of its name in the crate of every piece of Rust code of the account. **The utility class Tax** ```rust use crate::Round::cents; // another utility class pub const RATE: f64 = 0.2; pub fn add(amount: f64) -> f64 { cents(amount * (1.0 + RATE)) } ``` **A custom API which uses it** ```rust use am::{Errors, G, Value, json}; use crate::Tax; pub fn main(g: &mut G) -> Result { Ok(json!({ "total": Tax::add(g.req.body()["amount"].as_f64().unwrap_or(0.0)) })) } ``` - The name of the class is the name of the module : letters of a to z, digits and `_`, no word of Rust (`type`, `match`…) and none of `code`, `am`, `std`, `core`, `alloc`, `main`. Name it from the crate : `crate::Tax`. - The modules are compiled into each piece of code which runs : what a class keeps in a `static` is kept per piece of code. Use `g.shared` for what all code shares. - In the Git repository the class is `src/utility_classes/---/.rs`, declared `pub mod ;` in `am/rust/project.rs` : `cargo check` compiles the same paths. ## Test cases A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) written in Rust names its tests in `am::tests!`, in the place of a `main`. A test is a `fn(g: &mut G) -> Result<(), Errors>` : the errors it returns, or what it panics with (`assert_eq!`), are its failure. **A test case** ```rust use am::{Errors, G, Mock, RunCustomApi, json}; fn the_cost_of_a_parcel(g: &mut G) -> Result<(), Errors> { g.sys.test.mock(Mock::new("db.getAll").when(json!({ "collection": "rates" })).returns(json!([{ "per_kg": 2.5 }])))?; let out = g.sys.test.run_custom_api(RunCustomApi::new("shippingCost").body(json!({ "weight": 4 })))?; assert_eq!(out["cost"].as_f64(), Some(10.0)); Ok(()) } am::tests!(the_cost_of_a_parcel); ``` - `g.sys.test` : `run_custom_api(RunCustomApi)`, `run_custom_api_full(RunCustomApi)` for the whole answer (`data`, `status_code`, `headers`, `logs`), `mock(Mock)`, `mocks(Vec)`, `clear_mocks(Option<&str>)`, `calls(Option<&str>)`. - The custom API under test can be written in any language. ## Crates Add `uuid[v4]` `1` and `serde[derive]` `1` in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages), then : **Crates of crates.io** ```rust use am::{Errors, G, Value}; use serde::Serialize; #[derive(Serialize)] struct Ticket { id: String, seats: u32, } pub fn main(g: &mut G) -> Result { let ticket = Ticket { id: uuid::Uuid::new_v4().to_string(), seats: 2 }; Ok(serde_json::to_value(ticket)?) } ``` - `serde_json` and `serde` (with `derive`) are always there; add any other crate with its version requirement (`1`, `^1.40`, `>=1.2, <2`) and features (`tokio[rt-multi-thread,macros]`). - The crates are built with the runner when the image is built; while code runs nothing is downloaded. ## How it runs - Each piece of code is built once (`cargo build --release`, about a second or two once the crates are built) as a library of its own, loaded into the runner. The runner and your code exchange JSON only, through functions of C : no type of one ever crosses into the other. Saving starts the build in the background; code which does not compile is reported in the console of the account (`main.rs:4:8: error[E0425]: cannot find value missing`). - Every request runs on a thread of the runner; requests run at the same time, on every core. A thread takes the next request when its own is done : a `thread_local!` value of your code may be the one an earlier request left there. - `static` values live as long as the runner; `g.shared` is shared with every other piece of code of the sandbox. ## In the code editor The editor completes `g` as Rust writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor). **Where the editor helps** ```rust use am::{Errors, G, GetAll, Value}; pub fn main(g: &mut G) -> Result { // g. → req, res, sys, logger, shared g.sys.db. → get_all, get_by_id, query, get_all_gen, full… g.logger.log(&g.req.body()["name"]); // g.req. → body, query, headers, auth… g.res. → set_status_code, headers_mut… let rows = g.sys.db.get_all(GetAll::new("mysql_8", "inventory", "customers").limit(10))?; // GetAll:: → new, default GetAll::new(" → your instances, then its databases, then their tables // GetAll::new(…). → find, limit, sort, select… for row in &rows { g.logger.log(&row["first_name"]); // row[" → the columns of customers } Ok(rows[0]["first_name"].clone()) } ``` - After `GetAll::` the editor offers what makes the type (`new`, `default`), after `GetAll::new(…).` the methods which set its keys, each with what it is for. Inside `.find(json!({ "` it offers the columns of the table, inside `.select("` and `.sort("` too. - What a call answers has its type : on the rows of a `get_all` the columns are offered inside the quotes of an index (`rows[0]["`, `row["`) and of `get` (`row.get("`), in a `for` loop, in a closure (`.map(|row| row["…"])`) and in the function given to `get_all_by_stream`. `changed.` (an `update_many`) offers `updated_rows_count`, `saved.` offers `row`, `rows` and `is_list`, the whole answer of `g.sys.db.full` offers `success`, `data`, `errors`… - `am::` offers `fail` and what the crate has. - `g.req.` and `g.res.` offer a method for each key of the request and of the answer ; `g.req.auth().` the opened tokens, written as an index (`g.req.auth()["authAMUser"]`). - The columns follow a row the same way : `rows[0].` then `first_name` writes `rows[0]["first_name"]`. ## Reference | `g.` | Rust | |---|---| | `req` | `body()`, `query()`, `params()`, `headers()`, `auth()`, `req_info()`, `event_data()` (each a `&Value`, `…_mut()` to change it, `set_…` to replace it), `is_api_request_from_user()` ; a `Value` itself | | `res` | `set_status_code`, `set_content_type`, `headers_mut()`, `shared_mut()`, `output()`, `output_mut()`, `set_output`, `errors()`, `warnings()` ; a `Value` itself | | `sys.db` | `get_all(GetAll)`, `get_by_id(GetById)`, `query(Query)`, `save_single_or_multiple(Save)`, `master_save(Save)`, `array_operations(ArrayOperations)`, `update_by_id(UpdateById)`, `update_many(UpdateMany)`, `replace_by_id(UpdateById)`, `remove_by_id(RemoveById)`, `remove_by_query(RemoveByQuery)`, `aggregate(Aggregate)`, `count(Count)`, `distinct(Distinct)`, `distinct_query(Distinct)`, `get_all_by_stream`, `query_by_stream`, and the generated APIs `get_all_gen`… | | `sys.db.full`, `sys.system.full`, `sys.cache.full` | the same calls, each with its whole answer : `ApiResponse` (`success`, `status_code`, `data`, `total_count`, `errors`, `warnings`) | | `sys.system` | `encrypt`, `encrypt_with`, `decrypt`, `decrypt_with`, `hash`, `get_secret`, `get_secrets`, `call_external_api`, `call_external_apis`, `get_token`, `get_tokens`, `execute_query`, `get_table_meta`, `get_table_metas`, `create_indexes`, `drop_indexes`, `drop_indexes_of_many`, `get_indexes`, `get_indexes_of_many`, `emit_event`, `emit_event_ws`, `is_valid_data_for_table(s)`, `is_valid_data_for_custom_api(s)`, `multi_tenant_instance(s)_updated`, `is_valid_connection_string(s)` | | `sys.cache` | `get_key`, `get_keys`, `set_key`, `set_keys`, `remove_key`, `remove_keys`, `reset_cache_db`, `reset_cache_dbs`, `reset_cache_custom_apis`, `reset_cache_custom_apis_of_tenant`, `reset_cache_system_apis`, `reset_cache_system_apis_of_tenant` | | `sys.call("db.getAll", …)` | any API by its name in JavaScript, with its arguments as they are | | `logger` | `log`, `debug`, `info`, `warn`, `error` : a text or a `Value` | | `shared` | `get`, `set`, `update` | | errors | `am::fail(code, message)`, `Errors { list }`, `.message()`, `.code()` | ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) --- # Java > Write custom APIs, hooks, schedulers and event listeners of API Maker in Java. static Object main(G g) in a class, g.req, g.res, g.sys.db, am.Json, AmErrors, packages from Maven Central, with many examples. Source: https://docs.apimaker.dev/v4/docs/languages/java.html Code in Java is a class with `static Object main(G g)`. It is compiled once by the compiler of the JDK, and runs on the image of the JDK of Eclipse Temurin (`eclipse-temurin:25-jdk` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) name another version), with only the packages you add from Maven Central. **hello.java** ```java import am.G; import java.util.Map; class Api { static Object main(G g) { String name = g.req.getBody().getString("name", "world"); return Map.of("hello", name); } } ``` | | | |---|---| | Runtime | JDK 20 or later, a version of `eclipse-temurin:-jdk` : `25` (default), `21` | | `main` | a static method `main(G g)` of any class : it returns `Object`, any other type, or is `void` | | Class | `class Api` in a new item, not `public` : its file is named after the item (`orders.java`), and Java wants a public class in a file of its name. Every piece of code is compiled on its own, so each can have its own `Api`, in a sandbox and in [git](https://docs.apimaker.dev/v4/docs/Git/git.html#project-of-every-language) | | Answer | what `main` returns, written as JSON; `null` lets the API go on | | Calls of `g.sys` | typed : what a call takes is a class made key after key (`new GetAll("mysql_8", "inventory", "customers").limit(10)`), what it answers has its type (`List`, `JsonObject`, `long`, `UpdateManyResult`) | | Values | plain JSON : an object is an `am.JsonObject` (a `Map` which reads its keys by their types : `getString`, `getLong`, `getObject`…), then `List`, `String`, `Long` or `Double`, `Boolean`, `null` | | Imports | `am.G`, `am.GetAll`, `am.JsonObject`, `am.AmErrors`… : import what the code names, as in any Java file | | Packages | Maven Central : `com.google.code.gson:gson` `2.11.0` | | Printing | `System.out.println` goes to the console of the account | ## The request and the answer `g.req` and `g.res` have a getter and a setter for each key they are known to have : `g.req.getBody()`, `g.req.getQuery()`, `g.req.getParams()` and `g.req.getHeaders()` give the objects of the request, `g.res.setStatusCode(201)` sets the status of the answer. Each object is a `JsonObject`, which reads a key by its type without a cast : `g.req.getBody().getString("name")` is `null` when the key is not there, `getString("name", "world")` gives a value for that case. A body which is a list (`[{…}, {…}]`, several rows to save) is read with `g.req.getList("body")` ; `g.req.getBody()` is an empty `JsonObject` then. **Read the request, shape the answer** ```java import am.G; import am.JsonObject; class Api { static Object main(G g) { String status = g.req.getQuery().getString("status", "open"); boolean signed = g.req.getHeaders().has("x-am-authorization"); g.res.setStatusCode(201); g.res.getHeaders().put("X-Request-Kind", "orders"); return JsonObject.of("customer", g.req.getParams().get("id"), "status", status, "signed", signed); } } ``` - `JsonObject.of("key", value, …)` makes an object from keys and values in turn (a value can be `null`) ; `Map.of(…)` and any other `Map` are objects too. A `JsonObject` reads a key as what you expect : `getString`, `getLong`, `getInt`, `getDouble`, `getBoolean` (`0`, `false` or the value you give when something else is there), `getObject` (an empty one when none is there), `getList`, `getObjects`, `getStrings`, and `at("address", "phones", 0, "number")` for a path. - A whole number of the request is a `Long`, any other number a `Double` : read numbers with `getLong` and `getDouble`, or as a `Number`, not with a cast to `Integer`. - A `Map`, a `List`, an array (a `byte[]` is bytes : see below), a record (its components), an object of a class of yours (its fields), an `Optional`, an enum (its name) : all are answers. `NaN` and infinities are `null`; a `java.util.Date` and the types of `java.time` are texts (`2026-01-02T03:04:05.678Z`). - What you change of `g.req` in a pre hook is what the API sees. ### Bytes as the answer A file, a picture, a PDF : return its bytes (a `byte[]`), or give them to `g.res.setOutput`, and name their kind of content. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. (A text under a kind of bytes, `image/gif`, is read as base64.) **A picture as the answer** ```java import am.G; import java.util.Base64; class Api { static Object main(G g) { byte[] picture = Base64.getDecoder().decode("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA="); g.res.setContentType("image/gif"); g.res.getHeaders().put("Content-Disposition", "inline; filename=\"pixel.gif\""); return picture; } } ``` ## Calling API Maker : `g.sys` Each method is a call of the server, with its types. What it takes is a class of `am` made with what it is about, then key after key (`GetAll`, `GetById`, `Count`…) ; what it answers is typed too : rows are a `List`, a row a `JsonObject` (`null` when there is none), a count a `long`, what an API describes a class with a getter for each key (`UpdateManyResult`). The thread of the request waits for the answer; the other requests go on. **Database calls** ```java import am.Count; import am.G; import am.GetAll; import am.GetById; import am.JsonObject; import java.util.List; import java.util.Map; class Api { static Object main(G g) { List rows = g.sys.db.getAll(new GetAll("mysql_8", "inventory", "customers").limit(10).sort("-customer_id")); JsonObject two = g.sys.db.getById(new GetById("mysql_8", "inventory", "customers", 2)); long count = g.sys.db.count(new Count("mysql_8", "inventory", "customers").find(Map.of("country", "IN"))); return JsonObject.of("first", rows.isEmpty() ? null : rows.get(0), "name", two.getString("first_name"), "inIndia", count); } } ``` **Save, update, remove** ```java import am.G; import am.JsonObject; import am.RemoveById; import am.Save; import am.Saved; import am.UpdateById; import java.util.Map; class Api { static Object main(G g) { Saved saved = g.sys.db.saveSingleOrMultiple(new Save("mysql_8", "inventory", "customers", JsonObject.of("first_name", "Ada", "last_name", "Lovelace"))); g.sys.db.updateById(new UpdateById("mysql_8", "inventory", "customers", 2).updateData(Map.of("first_name", "MALLORY"))); g.sys.db.removeById(new RemoveById("mysql_8", "inventory", "customers", 101)); // What a save answers : saved.row(), saved.rows(), saved.isList(). return saved.row(); } } ``` - A key the class has no method for is set as it is : `new GetAll("mysql_8", "inventory", "customers").with("cursor", "abc")`. A class is made of a map too : `new GetAll(g.req.getBody())`. - The generated APIs are under `g.sys.db.gen` (`getAllGen`…). The whole answer of a call (`isSuccess()`, `getStatusCode()`, `getData()`, `getTotalCount()`, `getErrors()`) : `g.sys.db.full.getAll(…)`, an `ApiResponse>`. Calls at the same time are threads. Virtual threads (JDK 21 and later) cost next to nothing : **Calls at the same time** ```java import am.Count; import am.G; import am.JsonObject; import java.util.concurrent.Executors; class Api { static Object main(G g) throws Exception { try (var threads = Executors.newVirtualThreadPerTaskExecutor()) { var orders = threads.submit(() -> g.sys.db.count(new Count("mongodb", "shop", "orders"))); var products = threads.submit(() -> g.sys.db.count(new Count("mongodb", "shop", "products"))); return JsonObject.of("orders", orders.get(), "products", products.get()); } } } ``` - `main` can say `throws Exception`. A call which fails in a thread comes out of `get()` : its errors are the errors of the answer. - On JDK 20 use `Executors.newCachedThreadPool()` : virtual threads are part of the JDK from 21 on. ### Rows as they arrive **Sum a large table** ```java import am.G; import am.GetAll; import java.util.Map; class Api { static Object main(G g) { double[] total = { 0 }; g.sys.db.getAllByStream(new GetAll("mysql_8", "inventory", "orders"), row -> total[0] += row.getDouble("amount")); return Map.of("amount", total[0]); } } ``` The function of the rows is an `am.Rows` (`row -> …`), and each row a `JsonObject`. A lambda of Java does not assign a local variable : keep the sum in an array of one, or in a `DoubleAdder`. What the function throws ends the call. ### System APIs and cache **Encrypt, hash, events and cache keys** ```java import am.G; import am.JsonObject; import java.util.List; import java.util.Map; class Api { static Object main(G g) { String sealedData = g.sys.system.encrypt(Map.of("card", "4111")); String digest = g.sys.system.hash("my text"); g.sys.system.emitEvent("order_created", Map.of("id", 7), List.of("notify_shop")); g.sys.cache.setKey("last_order", "7", 600); return JsonObject.of("sealedData", sealedData, "digest", digest, "last", g.sys.cache.getKey("last_order")); } } ``` ## Errors **Refuse a request** ```java import am.AmErrors; import am.G; import java.util.Map; class Api { static Object main(G g) { if (!g.req.getBody().has("quantity")) throw new AmErrors("'quantity' is missing.", 400); return Map.of("ok", true); } } ``` **Catch the errors of a call** ```java import am.AmErrors; import am.G; import am.RemoveById; import java.util.Map; class Api { static Object main(G g) { try { g.sys.db.removeById(new RemoveById("mysql_8", "inventory", "customers", 404)); return Map.of("removed", true); } catch (AmErrors refused) { g.logger.warn("not removed :", refused.getMessage()); return Map.of("removed", false, "code", refused.code); } } } ``` - `AmErrors` is a `RuntimeException` : nothing has to be declared. `refused.errors` is the list of the errors (each a `ResponseError` : `getMessage()`, `getCode()`, `getField()`…), `refused.code` the code of the first one. Several errors at once : `throw new AmErrors(List.of(Map.of("code", 400, "message", "…"), …))`. - Any other exception answers `500` with its type and message, the message of what it happened because of, and the lines of your code : `at Api.helper(orders.java:7)`. ## Logs, printing and shared values **Logs and shared values** ```java import am.G; import java.util.Map; class Api { static Object main(G g) { g.logger.log("order", 7, "paid"); // "order 7 paid" g.logger.error(Map.of("step", "payment")); // maps, lists and records as indented JSON System.out.println("kept in the console of the account"); // Lives as long as the runner, shared by every piece of code it runs. return g.shared.merge("calls", 1, (old, one) -> (Integer) old + 1); } } ``` ## Hooks **Pre hook : only my rows** ```java import am.G; class Api { static Object main(G g) { // objectAt : the object at a key, made and kept there when none is. g.req.getQuery().objectAt("find").put("owner_id", g.req.getAuth().getObject("authAMDB").get("id")); return null; } } ``` **Post hook : hide a column** ```java import am.G; import java.util.List; import java.util.Map; class Api { static Object main(G g) { if (g.res.getOutput() instanceof List rows) { for (Object row : rows) { if (row instanceof Map object) object.remove("password"); } } return null; } } ``` ## Utility classes A [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in Java is a class of its name. The utility classes of Java of an account are compiled together, and the other Java code of the account names what they declare, without an import. **The utility class Tax** ```java public class Tax { public static final double RATE = 0.2; public static double add(double amount) { return Round.cents(amount * (1 + RATE)); // Round : another utility class } } ``` **A custom API which uses it** ```java import am.G; import java.util.Map; class Api { static Object main(G g) { return Map.of("total", Tax.add(g.req.getBody().getDouble("amount"))); } } ``` - The class has the name of the utility class (`public class Tax` of the class `Tax`) : letters of a to z, digits and `_`, no digit first, no word of Java (`record`, `class`…) and not `am`. One class of a name per account, whatever its folder : the classes are in no package, so write no `package` line. - The classes are loaded once per runner and shared by all Java code of the account there : what a class keeps in a static field is kept there. A class which is saved again is compiled anew with the others, and the code which uses them is compiled again on its next run. - A class of the code comes before a utility class of the same name : every piece of code keeps its own `Api`. - What does not compile in a class is said with its file and its line : `src/utility_classes/utils---Tax/Tax.java:3: error: cannot find symbol…`. - In the Git repository the class is `src/utility_classes/---/.java` : the project of Gradle compiles the classes together, then every piece of code with them. ## Test cases A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) written in Java has a method per test : every static method whose name starts with `test` and which takes `G g`, in any class. What it throws is its failure. **A test case** ```java import am.G; import am.JsonObject; import am.Mock; import am.RunCustomApi; import java.util.List; import java.util.Map; class ShippingTests { static void testTheCostOfAParcel(G g) { g.sys.test.mock(new Mock("db.getAll").when(Map.of("collection", "rates")).returns(List.of(Map.of("per_kg", 2.5)))); JsonObject output = JsonObject.from(g.sys.test.runCustomApi(new RunCustomApi("shippingCost").body(Map.of("weight", 4)))); if (output.getDouble("cost") != 10) throw new IllegalStateException("the cost is " + output.get("cost")); } } ``` - `g.sys.test` : `runCustomApi(new RunCustomApi(…))`, `runCustomApiFull(…)` for the whole answer (`getData()`, `getStatusCode()`, `getHeaders()`, `getLogs()`), `mock(new Mock(…))`, `mocks(List)`, `clearMocks()` or `clearMocks(method)`, `calls()` or `calls(method)`. - The tests run in the order they are written. The custom API under test can be written in any language. - Throw to fail : the `assert` of Java is checked only by a JVM started with `-ea`, and the runner is not. - The lines of a test run are counted on JDK 24 and later. On an older JDK the tests run as well, and the coverage has no lines of Java. ## Packages Add `com.google.code.gson:gson` in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages), then : **A package of Maven Central** ```java import am.G; import com.google.gson.Gson; import java.util.Map; class Api { static Object main(G g) { return Map.of("json", new Gson().toJson(java.util.List.of(1, 2))); } } ``` - A package is named by its group and its artifact : `group:artifact`. Versions are the ones of Maven : `2.11.0`, `33.4.0-jre`, a range `[1.0,2.0)`; empty is the latest release. - Maven resolves the packages, with what they need, when the image is built; while code runs nothing is downloaded. Code which imports what is not there is told so : `'com.google.gson' is not there : is its package in the packages of Java in the settings of the sandbox?`. - [Kotlin](https://docs.apimaker.dev/v4/docs/languages/kotlin.html) has packages of its own, from Maven Central too : a package added to Java is not there for Kotlin code. ## How it runs - Each piece of code is compiled once, with its lines : stack traces say them. Saving starts that compilation in the background, so the first request does not wait for it, and code which does not compile is reported in the console of the account (`main.java:3: error: ';' expected`). - Every request runs on a thread of its own; requests run at the same time, on every core. - Static fields of your code live as long as the runner, per piece of code : protect them (`AtomicLong`, `synchronized`), or use `g.shared`. - Each piece of code has a class loader of its own : two pieces of code never see each other's classes, only the utility classes of the account. ## In the code editor The editor completes `g` as Java writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor). **Where the editor helps** ```java import am.G; import am.GetAll; import am.JsonObject; import java.util.List; class Api { static Object main(G g) { // g. → req, res, sys, logger, shared g.sys.db. → getAll, getById, query, gen, full… g.logger.log(g.req.getBody()); // g.req. → getBody, getQuery, getHeaders, getAuth… g.res. → setStatusCode, getHeaders… List rows = g.sys.db.getAll(new GetAll("mysql_8", "inventory", "customers").limit(10)); // new GetAll(" → your instances, then its databases, then their tables // new GetAll(…). → find, limit, sort, select… for (JsonObject row : rows) { g.logger.log(row.getString("first_name")); // row.getString(" → the columns of customers } return rows.get(0).getString("first_name"); } } ``` - After `new GetAll(…).` the editor offers the methods which set its keys, each with what it is for ; a method gives the same value, so the next key follows. Inside `.find(JsonObject.of("` (or `Map.of("`) it offers the columns of the table, inside `.select("` and `.sort("` too. - What a call answers has its type : on the rows of a `getAll` the columns are offered inside the quotes of `getString`, `getLong`, `getDouble`, `getObject`… and of `get` (`row.get("`), in a `for` loop and in a lambda (`rows.forEach(row -> …)`). `changed.` (an `updateMany`) offers `getUpdatedRowsCount`, `saved.` offers `row`, `rows` and `isList`, the whole answer of `g.sys.db.full` offers `isSuccess`, `getData`, `getErrors`… - `g.req.` and `g.res.` offer a getter and a setter for each key of the request and of the answer ; `g.req.getAuth().` the opened tokens, written `g.req.getAuth().get("authAMUser")`. - A value your code says is an object has its keys and what a `JsonObject` has : `JsonObject.from(value).` and `((JsonObject) value).`. ## Reference | `g.` | Java | |---|---| | `req` | `am.Request` : `getBody()`, `getQuery()`, `getParams()`, `getHeaders()`, `getAuth()`, `getReqInfo()` (each a `JsonObject`), `getEventData()`, `isApiRequestFromUser()`, and a setter for each ; a `JsonObject` itself | | `res` | `am.Response` : `setStatusCode`, `setContentType`, `getHeaders()`, `getShared()`, `getOutput()`, `setOutput`, `getErrors()`, `getWarnings()` ; a `JsonObject` itself | | `sys.db` | `getAll(GetAll)`, `getById(GetById)`, `query(Query)`, `saveSingleOrMultiple(Save)`, `masterSave(Save)`, `arrayOperations(ArrayOperations)`, `updateById(UpdateById)`, `updateMany(UpdateMany)`, `replaceById(UpdateById)`, `removeById(RemoveById)`, `removeByQuery(RemoveByQuery)`, `aggregate(Aggregate)`, `count(Count)`, `distinct(Distinct)`, `distinctQuery(Distinct)`, `getAllByStream`, `queryByStream` ; `sys.db.gen.getAllGen`… | | `sys.db.full`, `sys.db.gen.full`, `sys.system.full`, `sys.cache.full` | the same calls, each with its whole answer : `ApiResponse` (`isSuccess()`, `getStatusCode()`, `getData()`, `getTotalCount()`, `getErrors()`, `getWarnings()`) | | `sys.system` | `encrypt`, `encryptWith`, `decrypt`, `decryptWith`, `hash`, `getSecret`, `getSecrets`, `callExternalApi`, `callExternalApis`, `getToken`, `getTokens`, `executeQuery`, `getTableMeta`, `getTableMetas`, `createIndexes`, `dropIndexes`, `dropIndexesOfMany`, `getIndexes`, `getIndexesOfMany`, `emitEvent`, `emitEventWS`, `isValidDataForTable(s)`, `isValidDataForCustomAPI(s)`, `multiTenantInstance(s)Updated`, `isValidConnectionString(s)` | | `sys.cache` | `getKey`, `getKeys`, `setKey`, `setKeys`, `removeKey`, `removeKeys`, `resetCacheDB`, `resetCacheDBs`, `resetCacheCustomApis`, `resetCacheCustomApisOfTenant`, `resetCacheSystemApis`, `resetCacheSystemApisOfTenant` | | `sys.call("db.getAll", …)` | any API by its name in JavaScript, with its arguments as they are | | `JsonObject` | `of`, `from`, `getString`, `getLong`, `getInt`, `getDouble`, `getBoolean`, `getObject`, `objectAt`, `getList`, `getObjects`, `getStrings`, `has`, `with`, `at` | | `logger` | `log`, `debug`, `info`, `warn`, `error` | | `shared` | a `ConcurrentHashMap` of the runner | | `Json` | `stringify`, `pretty`, `parse`, and the paths of any value : `get`, `text`, `number`, `integer`, `truth`, `object`, `array` | | errors | `throw new AmErrors(message, code)` or `new AmErrors(list)`; `.errors` (a `List`), `.code` | ## Related - [Kotlin](https://docs.apimaker.dev/v4/docs/languages/kotlin.html) · [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) --- # Kotlin > Write custom APIs, hooks, schedulers and event listeners of API Maker in Kotlin. fun main(g: G): Any?, g.req.body, g.res, the typed calls of g.sys.db (GetAll, List), mapOf and data classes as JSON, AmErrors, packages from Maven Central, with many examples. Source: https://docs.apimaker.dev/v4/docs/languages/kotlin.html Code in Kotlin defines `fun main(g: G): Any?` at the top of its file. It is compiled once by the compiler of Kotlin, and runs on the image of the JDK of Eclipse Temurin (`eclipse-temurin:25-jdk` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) name another version), with only the packages you add from Maven Central. **hello.kt** ```kotlin import am.G fun main(g: G): Any? { return mapOf("hello" to g.req.body.getString("name", "world")) } ``` | | | |---|---| | Runtime | Kotlin on the JDK, 20 or later : a version of `eclipse-temurin:-jdk`. The version of the settings is the one of the JDK : `25` (default), `21` | | Compiler | Kotlin `2.4.20`, or the one the [Dockerfile](https://docs.apimaker.dev/v4/docs/languages/kotlin.html#the-compiler) names | | `main` | a function `main(g: G)` at the top of the file : it returns `Any?`, any other type, or nothing | | Answer | what `main` returns, written as JSON; `null` lets the API go on | | Calls of `g.sys` | typed : what a call takes is a class made key after key (`GetAll("mysql_8", "inventory", "customers").limit(10)`), what it answers has its type (`List`, `JsonObject?`, `Long`, `UpdateManyResult`) | | Values | plain JSON : an object is an `am.JsonObject` (a `MutableMap` which reads its keys by their types : `getString`, `getLong`, `getObject`…), then `List`, `String`, `Long` or `Double`, `Boolean`, `null`. `mapOf`, `listOf` and data classes are answers | | Imports | `am.G`, `am.GetAll`, `am.JsonObject`, `am.AmErrors`… : import what the code names | | Packages | Maven Central : `com.google.code.gson:gson` `2.11.0` | | Printing | `println` goes to the console of the account | ## The request and the answer `g.req` and `g.res` have a property for each key they are known to have : `g.req.body`, `g.req.query`, `g.req.params` and `g.req.headers` are the objects of the request, `g.res.statusCode = 201` sets the status of the answer. Each object is a `JsonObject` : a map of JSON you read and write with `[]`, which also reads a key by its type (`g.req.body.getString("name", "world")`). A body which is a list (`[{…}, {…}]`, several rows to save) is read with `g.req.getList("body")` ; `g.req.body` is an empty `JsonObject` then. **Read the request, shape the answer** ```kotlin import am.G fun main(g: G): Any? { val status = g.req.query.getString("status", "open") val signed = g.req.headers.has("x-am-authorization") g.res.statusCode = 201 g.res.headers["X-Request-Kind"] = "orders" return mapOf("customer" to g.req.params["id"], "status" to status, "signed" to signed) } ``` - A key which is not there is `null`. A `JsonObject` reads a key as what you expect : `getString`, `getLong`, `getInt`, `getDouble`, `getBoolean` (`0`, `false` or the value you give when something else is there), `getObject` (an empty one when none is there), `getList`, `getObjects`, `getStrings`, and `at("address", "phones", 0, "number")` for a path. - `objectAt("find")` gives the object at a key to change it : it is made and kept there when none is (`g.req.query.objectAt("find")["owner_id"] = 5`). - A whole number of the request is a `Long`, any other number a `Double` : read a number with `getLong`, `getInt` and `getDouble`, or as a `Number` (`(g.req.body["quantity"] as Number).toInt()`). - A `Map`, a `List`, a `Set`, an array (a `ByteArray` is bytes : see below), a data class (its properties), an object of a class of yours, an enum (its name) : all are answers. `NaN` and infinities are `null`; the types of `java.time` are texts (`2026-01-02T03:04:05.678Z`). - What you change of `g.req` in a pre hook is what the API sees. `mapOf` gives a map which is read only : put a `mutableMapOf` into the request when the code changes it later. ### Bytes as the answer A file, a picture, a PDF : return its bytes (a `ByteArray`), or put them in `g.res.output`, and name their kind of content. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. (A text under a kind of bytes, `image/gif`, is read as base64.) **A picture as the answer** ```kotlin import am.G import java.util.Base64 fun main(g: G): Any? { val picture = Base64.getDecoder().decode("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=") g.res.contentType = "image/gif" g.res.headers["Content-Disposition"] = "inline; filename=\"pixel.gif\"" return picture } ``` ## Calling API Maker : `g.sys` Each method is a call of the server, with its types. What it takes is a class of `am` made with what it is about, then key after key (`GetAll`, `GetById`, `Count`…) ; what it answers is typed too : rows are a `List`, a row a `JsonObject?`, a count a `Long`, what an API describes a class whose keys are properties (`changed.updatedRowsCount`). The generated APIs are under `g.sys.db.gen`, the whole answer of a call under `full` : `g.sys.db.full.getAll(…)` gives an `ApiResponse>` (`isSuccess`, `data`, `totalCount`, `errors`). The thread of the request waits for the answer; the other requests go on. **Database calls** ```kotlin import am.Count import am.G import am.GetAll import am.GetById fun main(g: G): Any? { val rows = g.sys.db.getAll(GetAll("mysql_8", "inventory", "customers").limit(10).sort("-customer_id")) val two = g.sys.db.getById(GetById("mysql_8", "inventory", "customers", 2)) val count = g.sys.db.count(Count("mysql_8", "inventory", "customers").find(mapOf("country" to "IN"))) return mapOf("first" to rows.firstOrNull(), "name" to two?.getString("first_name"), "inIndia" to count) } ``` **Save, update, remove** ```kotlin import am.G import am.RemoveById import am.Save import am.UpdateById fun main(g: G): Any? { val saved = g.sys.db.saveSingleOrMultiple(Save("mysql_8", "inventory", "customers", mapOf("first_name" to "Ada", "last_name" to "Lovelace"))) g.sys.db.updateById(UpdateById("mysql_8", "inventory", "customers", 2).updateData(mapOf("first_name" to "MALLORY"))) g.sys.db.removeById(RemoveById("mysql_8", "inventory", "customers", 101)) // What a save answers : saved.row(), saved.rows(), saved.isList. return saved.row() } ``` Calls at the same time are threads. Virtual threads (JDK 21 and later) cost next to nothing : **Calls at the same time** ```kotlin import am.Count import am.G import java.util.concurrent.Callable import java.util.concurrent.Executors fun main(g: G): Any? = Executors.newVirtualThreadPerTaskExecutor().use { threads -> val orders = threads.submit(Callable { g.sys.db.count(Count("mongodb", "shop", "orders")) }) val products = threads.submit(Callable { g.sys.db.count(Count("mongodb", "shop", "products")) }) mapOf("orders" to orders.get(), "products" to products.get()) } ``` - A call which fails in a thread comes out of `get()` : its errors are the errors of the answer. - Coroutines are a package, not a part of the language : add `org.jetbrains.kotlinx:kotlinx-coroutines-core` to the [packages](https://docs.apimaker.dev/v4/docs/languages/kotlin.html#packages) of Kotlin to use `runBlocking`, `async` and `await`. ### Rows as they arrive **Sum a large table** ```kotlin import am.G import am.GetAll fun main(g: G): Any? { var total = 0.0 g.sys.db.getAllByStream(GetAll("mysql_8", "inventory", "orders")) { row -> total += row.getDouble("amount") } return mapOf("amount" to total) } ``` The function of the rows changes your variables as any lambda of Kotlin does. What it throws ends the call. ### System APIs and cache **Encrypt, hash, events and cache keys** ```kotlin import am.G fun main(g: G): Any? { val sealedData = g.sys.system.encrypt(mapOf("card" to "4111")) val digest = g.sys.system.hash("my text") g.sys.system.emitEvent("order_created", mapOf("id" to 7), listOf("notify_shop")) g.sys.cache.setKey("last_order", "7", 600) return mapOf("sealedData" to sealedData, "digest" to digest, "last" to g.sys.cache.getKey("last_order")) } ``` ## Errors **Refuse a request** ```kotlin import am.AmErrors import am.G fun main(g: G): Any? { if (!g.req.body.has("quantity")) throw AmErrors("'quantity' is missing.", 400) return mapOf("ok" to true) } ``` **Catch the errors of a call** ```kotlin import am.AmErrors import am.G import am.RemoveById fun main(g: G): Any? { try { g.sys.db.removeById(RemoveById("mysql_8", "inventory", "customers", 404)) return mapOf("removed" to true) } catch (refused: AmErrors) { g.logger.warn("not removed :", refused.message) return mapOf("removed" to false, "code" to refused.code) } } ``` - `refused.errors` is the list of the errors (each a `ResponseError` : `message`, `code`, `field`…), `refused.code` the code of the first one. Several errors at once : `throw AmErrors(listOf(mapOf("code" to 400, "message" to "…"), …))`. - Any other exception answers `500` with its type and message, the message of what it happened because of, and the lines of your code : `at OrdersKt.helper(orders.kt:8)`. ## Logs, printing and shared values **Logs and shared values** ```kotlin import am.G fun main(g: G): Any? { g.logger.log("order", 7, "paid") // "order 7 paid" g.logger.error(mapOf("step" to "payment")) // maps, lists and data classes as indented JSON println("kept in the console of the account") // Lives as long as the runner, shared by every piece of code it runs. return g.shared.merge("calls", 1) { old, _ -> (old as Int) + 1 } } ``` ## Hooks **Pre hook : only my rows** ```kotlin import am.G fun main(g: G): Any? { // objectAt : the object at a key, made and kept there when none is. g.req.query.objectAt("find")["owner_id"] = g.req.auth.getObject("authAMDB")["id"] return null } ``` **Post hook : hide a column** ```kotlin import am.G fun main(g: G): Any? { for (row in g.res.output as? List<*> ?: emptyList()) { (row as? MutableMap<*, *>)?.remove("password") } return null } ``` ## Utility classes A [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in Kotlin starts as an `object` of its name. The utility classes of Kotlin of an account are compiled together, and the other Kotlin code of the account names what they declare, without an import. **The utility class Tax** ```kotlin object Tax { const val RATE = 0.2 fun add(amount: Double): Double = Round.cents(amount * (1 + RATE)) // Round : another utility class } ``` **A custom API which uses it** ```kotlin import am.G fun main(g: G): Any? = mapOf("total" to Tax.add(g.req.body.getDouble("amount"))) ``` - What else the file declares at its top (functions, classes, constants) is there for the code as well : `fun addTax(amount: Double)` of the class is `addTax(100.0)` in the code. - The name of the utility class : letters of a to z, digits and `_`, no digit first, no word of Kotlin (`object`, `when`…) and not `am`. One class of a name per account, whatever its folder : the classes are in no package, so write no `package` line. - The classes are loaded once per runner and shared by all Kotlin code of the account there : what an `object` keeps is kept there. A class which is saved again is compiled anew with the others, and the code which uses them is compiled again on its next run. - What the code declares comes before what a utility class declares under the same name. - What does not compile in a class is said with its file and its line : `src/utility_classes/utils---Tax/Tax.kt:3:16: error: unresolved reference 'missing'.` - In the Git repository the class is `src/utility_classes/---/.kt` : the project of Gradle compiles the classes together, then every piece of code with them. ## Test cases A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) written in Kotlin has a function per test : every function at the top of the file whose name starts with `test` and which takes `g: G`. What it throws is its failure. **A test case** ```kotlin import am.G import am.JsonObject import am.Mock import am.RunCustomApi fun testTheCostOfAParcel(g: G) { g.sys.test.mock(Mock("db.getAll").whenever(mapOf("collection" to "rates")).returns(listOf(mapOf("per_kg" to 2.5)))) val output = JsonObject.from(g.sys.test.runCustomApi(RunCustomApi("shippingCost").body(mapOf("weight" to 4)))) check(output.getDouble("cost") == 10.0) { "the cost is " + output["cost"] } } ``` - `g.sys.test` : `runCustomApi(RunCustomApi(…))`, `runCustomApiFull(…)` for the whole answer (`data`, `statusCode`, `headers`, `logs`), `mock(Mock(…))`, `mocks(listOf(…))`, `clearMocks()` or `clearMocks(method)`, `calls()` or `calls(method)`. The key `when` of a mock is set by `whenever` : `when` is a word of Kotlin. - The tests run in the order they are written. The custom API under test can be written in any language. - `check(…) { … }` and `require(…) { … }` always check. `assert(…)` is checked only by a JVM started with `-ea`, and the runner is not. - The lines of a test run are counted on JDK 24 and later. On an older JDK the tests run as well, and the coverage has no lines of Kotlin. ## Packages Add `com.google.code.gson:gson` in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages), then : **A package of Maven Central** ```kotlin import am.G import com.google.gson.Gson fun main(g: G): Any? = mapOf("json" to Gson().toJson(listOf(1, 2))) ``` - A package is named by its group and its artifact : `group:artifact`. Versions are the ones of Maven : `2.11.0`, `33.4.0-jre`, a range `[1.0,2.0)`; empty is the latest release. - Maven resolves the packages, with what they need, when the image is built; while code runs nothing is downloaded. Code which imports what is not there is told so : `'com.google.gson.Gson' is not there : is its package in the packages of Kotlin in the settings of the sandbox?`. - The standard library of Kotlin is always there. Coroutines (`org.jetbrains.kotlinx:kotlinx-coroutines-core`), serialization and reflection are packages. - [Java](https://docs.apimaker.dev/v4/docs/languages/java.html) has packages of its own, from Maven Central too : a package added to Kotlin is not there for Java code. ### The compiler The image gets the compiler of Kotlin `2.4.20` from Maven Central when it is built. Another version is a line of the [Dockerfile](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#dockerfile) of Kotlin, above the packages : **The Dockerfile of Kotlin with another compiler** ```dockerfile FROM {{AM_BASE_IMAGE}} ENV AM_KOTLIN_VERSION=2.3.0 {{AM_PACKAGES}} {{AM_RUNNER}} ``` ## How it runs - Each piece of code is compiled once, with its lines : stack traces say them. Saving starts that compilation in the background, so the first request does not wait for it, and code which does not compile is reported in the console of the account (`main.kt:4:12: error: unresolved reference 'nothing'.`). - Every request runs on a thread of its own; requests run at the same time, on every core. - Variables at the top of your file live as long as the runner, per piece of code : protect them (`AtomicLong`, `@Synchronized`), or use `g.shared`. - Each piece of code is compiled and loaded on its own : two pieces of code can declare functions and classes of the same names, and see only the utility classes of the account. - The code is compiled for the JVM 17 and runs on the JDK of the settings : what that JDK has, the code can use. ## In the code editor The editor completes `g` as Kotlin writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor). **Where the editor helps** ```kotlin import am.G import am.GetAll fun main(g: G): Any? { // g. → req, res, sys, logger, shared g.sys.db. → getAll, getById, query, gen, full… g.logger.log(g.req.body) // g.req. → body, query, headers, auth… g.res. → statusCode, headers… val rows = g.sys.db.getAll(GetAll("mysql_8", "inventory", "customers").limit(10)) // GetAll(" → your instances, then its databases, then their tables // GetAll(…). → find, limit, sort, select… for (row in rows) { g.logger.log(row.getString("first_name")) // row.getString(" → the columns of customers } return rows[0].getString("first_name") } ``` - After `GetAll(…).` the editor offers the methods which set its keys, each with what it is for ; a method gives the same value, so the next key follows. Inside `.find(mapOf("` it offers the columns of the table, inside `.select("` and `.sort("` too. - What a call answers has its type : on the rows of a `getAll` the columns are offered inside the quotes of `getString`, `getLong`, `getDouble`, `getObject`… and of an index (`row["`), in a `for` loop, in `forEach { row -> … }` and in the function given to `getAllByStream`, also under its name `it`. `changed.` (an `updateMany`) offers `updatedRowsCount`, `saved.` offers `row`, `rows` and `isList`, the whole answer of `g.sys.db.full` offers `isSuccess`, `data`, `errors`… - `g.req.` and `g.res.` offer a property for each key of the request and of the answer ; `g.req.auth.` the opened tokens, written as an index (`g.req.auth["authAMUser"]`). A value your code says is an object has its keys and what a `JsonObject` has : `JsonObject.from(value).`, `(value as JsonObject).`. ## Reference | `g.` | Kotlin | |---|---| | `req` | `am.Request` : `body`, `query`, `params`, `headers`, `auth`, `reqInfo` (each a `JsonObject`), `eventData`, `isApiRequestFromUser` ; a `JsonObject` itself | | `res` | `am.Response` : `statusCode`, `contentType`, `headers`, `shared`, `output`, `errors`, `warnings` ; a `JsonObject` itself | | `sys.db` | `getAll(GetAll)`, `getById(GetById)`, `query(Query)`, `saveSingleOrMultiple(Save)`, `masterSave(Save)`, `arrayOperations(ArrayOperations)`, `updateById(UpdateById)`, `updateMany(UpdateMany)`, `replaceById(UpdateById)`, `removeById(RemoveById)`, `removeByQuery(RemoveByQuery)`, `aggregate(Aggregate)`, `count(Count)`, `distinct(Distinct)`, `distinctQuery(Distinct)`, `getAllByStream`, `queryByStream` ; `sys.db.gen.getAllGen`… | | `sys.db.full`, `sys.db.gen.full`, `sys.system.full`, `sys.cache.full` | the same calls, each with its whole answer : `ApiResponse` (`isSuccess`, `statusCode`, `data`, `totalCount`, `errors`, `warnings`) | | `sys.system` | `encrypt`, `encryptWith`, `decrypt`, `decryptWith`, `hash`, `getSecret`, `getSecrets`, `callExternalApi`, `callExternalApis`, `getToken`, `getTokens`, `executeQuery`, `getTableMeta`, `getTableMetas`, `createIndexes`, `dropIndexes`, `dropIndexesOfMany`, `getIndexes`, `getIndexesOfMany`, `emitEvent`, `emitEventWS`, `isValidDataForTable(s)`, `isValidDataForCustomAPI(s)`, `multiTenantInstance(s)Updated`, `isValidConnectionString(s)` | | `sys.cache` | `getKey`, `getKeys`, `setKey`, `setKeys`, `removeKey`, `removeKeys`, `resetCacheDB`, `resetCacheDBs`, `resetCacheCustomApis`, `resetCacheCustomApisOfTenant`, `resetCacheSystemApis`, `resetCacheSystemApisOfTenant` | | `sys.call("db.getAll", …)` | any API by its name in JavaScript, with its arguments as they are | | `JsonObject` | `of`, `from`, `getString`, `getLong`, `getInt`, `getDouble`, `getBoolean`, `getObject`, `objectAt`, `getList`, `getObjects`, `getStrings`, `has`, `with`, `at` | | `logger` | `log`, `debug`, `info`, `warn`, `error` | | `shared` | a `ConcurrentHashMap` of the runner | | `Json` | `stringify`, `pretty`, `parse`, and the paths of any value : `get`, `text`, `number`, `integer`, `truth`, `array` | | errors | `throw AmErrors(message, code)` or `AmErrors(list)`; `.errors` (a `List`), `.code` | ## Related - [Java](https://docs.apimaker.dev/v4/docs/languages/java.html) · [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) --- # Table Schema > The schema of a table in API Maker - types, conversions, validations, defaults, keys, relations, virtual fields, auto increment, encryption and hashing - written in TypeScript, checked against the code, with the errors each rule gives and a field to copy for each rule. Source: https://docs.apimaker.dev/v4/docs/schema/schema.html A schema describes one table or collection : the type of every field, how a value is cleaned before it is saved, what makes it invalid, which fields point to other tables. It is a TypeScript file kept with the table in Git, whatever [language](https://docs.apimaker.dev/v4/docs/languages/index.html#basic-info) your code is written in, and it is what the [schema APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) enforce. The [generated APIs](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.html#without-a-schema) never read it. > Diagram : The stages of a write through a schema API, in order, from the request to 201 or 400 > > What a schema API does to every write : refuses unknown keys, converts the types, trims and cases strings, runs your conversion function, encrypts or hashes, fills ids and defaults, runs the rules and your validator function, then saves and answers 201, or answers 400 with every error at once and saves nothing. | | | |---|---| | Where | `APIs & Logic → Instance API (DB API)` : pick the instance, the database and the table, then the schema editor. **Generate** writes a first schema from the columns or from the documents of the table. | | Shape | `const schema: ISchemaType = { field: EType.string, other: { __type, validations, conversions, … } }` and `module.exports = { schema }`. | | Used by | The `/api/schema/…` APIs : save, master save, update, replace, get with `deep`, the [is valid data for table](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-table-api.html) check. | | Gives | Validation, conversions, defaults, relations, the [TypeScript interface](https://docs.apimaker.dev/v4/docs/features/developer-tools.html#typescript-interfaces-from-your-schemas) `db...I
`, the sample payloads of the API testing page and of Swagger. | ## 1. A schema, field by field **orders** ```typescript import { EType, ISchemaType, ISchemaProperty } from 'types'; const schema: ISchemaType = { _id: { __type: EType.objectId, isPrimaryKey: true, isAutoGenerateByAM: { valueGeneratorType: 'ObjectID' } }, order_no: { __type: EType.number, isAutoIncrementByAM: { start: 1, step: 1 } }, customer_id: { __type: EType.number, validations: { required: true }, database: 'shop', collection: 'customers', column: 'customer_id' }, status: { __type: EType.string, validations: { enum: ['PENDING', 'PAID', 'SHIPPED', 'CANCELLED'] }, conversions: { toUpperCase: true, defaults: { defaultValue: 'PENDING' } } }, total_cents: { __type: EType.number, validations: { min: 0 } }, created_at: { __type: EType.date, conversions: { defaults: { defaultFun: () => new Date() } } }, items: { __type: EType.objectId, isVirtualField: true, database: 'shop', collection: 'orderItems', s_columnVirtualLinker: '_id', t_columnVirtualLinker: 'order_id' }, version: { __type: EType.number, isConcurrencyControlField: true }, }; module.exports = { schema }; ``` - This is the `orders` table of the [sample shop](https://docs.apimaker.dev/v4/docs/getting-started/first-api.html) every new account gets : open it in the admin panel to see the whole thing with the `products`, `customers` and `orderItems` schemas. - A field is a type alone (`name: EType.string`) or an object with `__type` and the rules below. A key the schema does not have is refused on save : the error names it. - The examples of the sections below are single fields : paste them inside `const schema: ISchemaType = { … }`. ## 2. Types | `__type` | Holds | Notes | |---|---|---| | `EType.string` | text | `minLength`, `maxLength`, `email`, `enum`, trims and case conversions | | `EType.number` | numbers | `min`, `max`, auto increment | | `EType.boolean` | `true` / `false` | | | `EType.date` | dates | `min`, `max` ; sent and received as ISO strings | | `EType.objectId` | MongoDB ObjectId | strings in JSON | | `EType.file`, `EType.files` | uploaded files | the request schema of a custom API, not a table | **One field per type** ```typescript first_name: { __type: EType.string, validations: { required: true } }, employee_no: { __type: EType.number, validations: { required: true } }, is_active: { __type: EType.boolean }, joined_at: { __type: EType.date }, department_id: { __type: EType.objectId, validations: { required: true } }, ``` - MongoDB documents nest : `address: { city: EType.string }` is an embedded object, `tags: [EType.string]` an array of strings, `lines: [{ sku: EType.string, qty: EType.number }]` an array of objects. SQL tables are flat. - Columns of SQL which hold more than one value : a vector is `[EType.number]`, an array of PostgreSQL `[EType.string]`, and a JSON column `EType.string` which takes objects and lists. [Data types](https://docs.apimaker.dev/v4/docs/schema/data-types.html) has every type of every database. **Nested documents and arrays (MongoDB)** ```typescript address: { city: EType.string, zip: EType.string }, tags: [EType.string], lines: [{ sku: EType.string, qty: EType.number }], ``` ## 3. Conversions : clean the value first Conversions run before the validations, on save and on update. | Key | Does | |---|---| | `trim`, `trimStart`, `trimEnd` | Removes the spaces around a string. | | `toLowerCase`, `toUpperCase` | Changes the case. | | `conversionFun: (value, row) => …` | Your function ; its return value is stored. It runs even when the field is absent from the payload, which is how a `updated_at: () => new Date()` field is stamped on every save and update. | | `defaults` | `{ defaultValue }` or `{ defaultFun: () => … }` for a field absent from a save or a replace. `defaultValue` wins over `defaultFun`. `shouldReplaceNullWithDefault` and `shouldReplaceEmptyStringWithDefault` also replace `null` and `""`. | | `encryption: true` | Stored encrypted with `encryptionAlgorithm` and `secret` of the [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html). The same text encrypts differently every time. The APIs answer the value as it is stored : your code reads it back on the server with `g.sys.system.decrypt` and the same keys (see below). An object `{ encryptionAlgorithm, secret }` names other keys of the secret. | | `hashing: true` | Stored as an HMAC SHA-256 hash with the `nonce` (or `secret`) of the secret : passwords, tax ids. Look a hashed value up with the [hash data](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-hash-data-api.html) API. It is fast, with one key for every row and no salt per row : not a password hash like Argon2id or bcrypt. | **Trim and case** ```typescript first_name: { __type: EType.string, conversions: { trim: true } }, code: { __type: EType.string, conversions: { toUpperCase: true } }, email: { __type: EType.string, conversions: { toLowerCase: true, trimStart: true, trimEnd: true } }, ``` **conversionFun : change the value, or stamp it on every save and update** ```typescript city_name: { __type: EType.string, conversions: { conversionFun: (city_name, fullObj) => city_name ? city_name + '_IND' : city_name }, }, updated_at: { __type: EType.date, conversions: { conversionFun: () => new Date() }, // runs even when the field is absent from the payload }, ``` **defaults** ```typescript active: { __type: EType.boolean, conversions: { defaults: { defaultValue: true, shouldReplaceEmptyStringWithDefault: true, shouldReplaceNullWithDefault: true } }, }, created_at: { __type: EType.date, conversions: { defaults: { defaultFun: () => new Date() } }, // defaultValue wins over defaultFun when both are given }, ``` **Encrypted and hashed fields** ```typescript contact_email: { __type: EType.string, conversions: { encryption: true } }, // answered as stored : cipher text password: { __type: EType.string, conversions: { hashing: true } }, // compare with the hash data API ``` **Find by a hashed field, read an encrypted one back (custom API)** ```typescript // The same text always gives the same hash : hash what you look for const phone = await g.sys.system.hash(g.req.body.phone); const reviews = await g.sys.db.query({ instance: 'mongodb', database: 'shop', collection: 'reviews', find: { contact_phone: phone }, select: { review_no: 1, title: 1, contact_email: 1 }, }); // The keys the schema encrypted with : the plain text exists on the server only const [algorithm, pass] = await g.sys.system.getSecret(['common.encryptionAlgorithm', 'common.secret']); for (const review of reviews) { review.contact_email = await g.sys.system.decrypt(review.contact_email, algorithm, pass); } ``` ## 4. Validations : refuse a bad value | Key | Refuses | Error `type` | |---|---|---| | `required: true` | A missing, `null` or empty value on save. On update, only a `null` or empty value sent. | `required` | | `min`, `max` | A number or a date outside the range. | `min`, `max` | | `minLength`, `maxLength` | A string or an array outside the length range. | `minLength`, `maxLength` | | `email: true` | A string which is not an email address. | `emailNotValid` | | `enum: [ … ]` | A value outside the list. An empty list checks nothing. The sample data generator picks from the list. | `enumValidation` | | `validatorFun: (value, row) => …` | Your function, run last : return `true` to accept ; return `false` or a message, or throw, to refuse. | `invalidValue` | | `unique: true` | A value another row already has, or which appears twice in the same batch. Checked with a query before the write, so prefer a unique index for busy tables ([create indexes](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.html)). | `unique` | **required, min and max, minLength and maxLength** ```typescript registration_number: { __type: EType.number, validations: { required: true, min: 4, max: 10 } }, address: { __type: EType.string, validations: { required: true, minLength: 20, maxLength: 100 } }, ``` **email and enum** ```typescript mail_id: { __type: EType.string, validations: { required: true, email: true } }, country: { __type: EType.string, validations: { enum: ['India', 'Africa'] } }, ``` **validatorFun : your own rule, run last** ```typescript city_name: { __type: EType.string, validations: { required: true, validatorFun: (city_name, fullObj) => { if (city_name && ['AHMEDABAD', 'SURAT'].includes(city_name)) throw new Error(`You can not save city_name as '${city_name}'`); return true; }, }, }, ``` **unique : checked with a query before the write** ```typescript mobile_no: { __type: EType.number, validations: { unique: true } }, // prefer a unique index for busy tables ``` - The answer of a refused save is `400` with one entry per problem in `errors` : `{ type, field, message, code, dataIndex }`, `dataIndex` being the position of the row in an array body. See [Error codes](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html). ### Where the functions run `conversionFun`, `defaultFun` and `validatorFun` are JavaScript, whatever language the code of the account is written in. They run for whoever answers the save : the server for a request, or, for a save your code makes through `g.sys`, [the sandbox of that code](https://docs.apimaker.dev/v4/docs/languages/index.html#g-sys-in-the-sandbox). The schema itself is [settings](https://docs.apimaker.dev/v4/docs/languages/index.html#settings-run) : API Maker runs its file once, when you save it, and keeps every function as its text. A function therefore runs on its own later, without the file around it : it sees its arguments, `require` and what every script sees, not a variable or an import of the top of the schema. Inside a function write `require('dayjs')`, where it runs with the packages of its sandbox ; the top of the schema imports `types` and your utility classes written in TypeScript, nothing of npm. | The request is answered by | Its functions run in | |---|---| | The server, or code in TypeScript : a schema API called directly, a custom API or a hook in TypeScript | the sandbox of TypeScript of the account (Node.js or Bun), with the npm packages of its [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages) | | Code in another [language](https://docs.apimaker.dev/v4/docs/languages/index.html) : a custom API or a hook in Python, PHP, C#, Go, Rust, Java or Kotlin which saves through a schema | the sandbox of that language, in the JavaScript engine API Maker has there (QuickJS) | - A function answers the same in both : the values, the row it changes, the dates, the ids and what it throws. - The engine of the other languages has JavaScript itself (promises, `async`, `Date`, `JSON`, `Math`, regular expressions, `BigInt`), `require('types')`, `crypto` (`randomUUID`, `randomBytes`, `randomInt`, `getRandomValues`, `createHash` and `createHmac` with `md5`, `sha1`, `sha256`, `sha384` and `sha512`), `Buffer`, `console`, the timers and `process.env`. - It has no packages of npm and no `Intl` : `require('dayjs')` throws there (`Cannot find module 'dayjs' : … not the packages of npm.`), and `localeCompare` compares letter by letter, without the rules of a language. A function which needs one of them belongs to a table which is saved by code in TypeScript or through the schema APIs ; otherwise do that part in the code which saves. - An error JavaScript itself throws (calling what is no function, `BigInt(1.5)`) is worded by the engine which ran it, so its message can differ between the two. A message your function throws is the same. ## 5. Keys and generated values | Key | Meaning | |---|---| | `isPrimaryKey: true` | The primary key of the table : the id of get by id, update by id, remove by id. | | `isAutoIncrementByDB: true` | The database assigns the value (SQL identity columns). API Maker sends nothing for it. | | `isAutoIncrementByAM: true` or `{ start, step }` | API Maker assigns the next number, on every database, MongoDB included : see [Auto increment](https://docs.apimaker.dev/v4/docs/features/auto-increment.html). No effect with `isAutoIncrementByDB`. | | `isAutoGenerateByAM: { valueGeneratorType }` | A random id when the payload has none : `ObjectID`, `GUID_UUID`, `ULID` or `ShortUUID`. Wins over `isAutoIncrementByAM`. | | `isConcurrencyControlField: true` | The version field of [optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.html). | **Primary keys, generated ids and the version field** ```typescript customer_id: { __type: EType.number, isPrimaryKey: true, isAutoIncrementByAM: { start: 1000, step: 1 } }, id: { __type: EType.number, isPrimaryKey: true, isAutoIncrementByDB: true }, _id: { __type: EType.objectId, isPrimaryKey: true, isAutoGenerateByAM: { valueGeneratorType: 'ObjectID' } }, public_id: { __type: EType.string, isAutoGenerateByAM: { valueGeneratorType: 'ULID' } }, // GUID_UUID, ULID, ShortUUID, ObjectID version: { __type: EType.number, isConcurrencyControlField: true, conversions: { conversionFun: () => new Date().getTime() } }, ``` ## 6. Relations and virtual fields **A relation, its reverse, and a relation to another instance** ```typescript // orderItems.order_id points to orders._id order_id: { __type: EType.objectId, validations: { required: true }, database: 'shop', collection: 'orders', column: '_id' }, // orders.items : the line items of an order, read from orderItems through their order_id items: { __type: EType.objectId, isVirtualField: true, database: 'shop', collection: 'orderItems', s_columnVirtualLinker: '_id', t_columnVirtualLinker: 'order_id' }, // a relation to a table of another instance and database owner_id: { __type: EType.number, instance: 'mysql8', database: 'crm', table: 'owners', column: 'id' }, ``` | Key | Meaning | |---|---| | `collection` or `table`, `column` | The table and the column this field points to. `database` and `instance` when they differ from the ones of the schema. | | `isVirtualField: true` | A field which is not in the table : it is filled from the other table, and can be saved through master save. `s_columnVirtualLinker` is the column of this table, `t_columnVirtualLinker` the column of the other table which holds it. | - Relations feed [deep populate](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html) (`deep` on any get), [master save](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-master-save-api.html) (one call saves the order and its items) and the [ER diagram](https://docs.apimaker.dev/v4/docs/logs/logs-and-dashboards.html#er-diagram). - A virtual field can not be used in `find` : the error `virtualFieldUsedInFind` says so. ## 7. Other keys - A schema file can also export `validations`, a list of `SUPER_KEY` rules which refuse two rows with the same combination of fields. It is deprecated : create a unique index on the fields instead, it is faster and it holds for writes made outside API Maker too. ## Related - [Optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.html) · [Auto increment](https://docs.apimaker.dev/v4/docs/features/auto-increment.html) · [Deep populate](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html) · [Is valid data for table](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-table-api.html) - [Generated interfaces](https://docs.apimaker.dev/v4/docs/features/developer-tools.html#typescript-interfaces-from-your-schemas) · [Error codes](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) --- # Table Designer > Make and change tables in the admin panel of API Maker on every database - the four panels of instances, databases and tables, the designer and its script, editing a table with the statements shown first, the schema which follows its table, changes recorded into migration scripts, and editing a database. Source: https://docs.apimaker.dev/v4/docs/schema/table-designer.html The panels of `APIs & Logic → Instance API (DB API)` make and change the tables of every database API Maker connects to, and keep their [schemas](https://docs.apimaker.dev/v4/docs/schema/schema.html) in step. ## The four panels `APIs & Logic → Instance API (DB API)` has four panels, left to right : instances, databases, tables, APIs. Pick one in each and the next panel fills. | Panel | What it holds | |---|---| | Instances | A connection to a database server : MongoDB, MySQL (TiDB, Percona), MariaDB, SQL Server, PostgreSQL, Oracle. **Add New** takes the database type, a unique **Instance Name** (used in every URL and in code, it can not change later), the **Connection String** picked among the keys of the [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html) and **Max Pool Connections** ; Oracle adds the username, the password and the privilege. Saving tests the connection first : an instance which can not connect is not saved. **Compact Instance** (in the menu of a MongoDB instance) reclaims space on every database of the instance, one after the other ; the admin, config and local databases of MongoDB are left alone. | | Databases | The databases the connection sees, with their [settings](https://docs.apimaker.dev/v4/docs/settings/databaseSettings.html). **Add New** makes a database on every kind of server, with what its kind takes : the character set and the collation (MySQL, MariaDB), the owner, the template, the encoding and the locale (PostgreSQL), the collation (SQL Server), the password, the tablespace and the rights of its user (Oracle, where a database is a user) or its first collection (MongoDB) ; the statements are shown before they run. **Edit** renames a database or gives it other options ([below](https://docs.apimaker.dev/v4/docs/schema/table-designer.html#edit-database)) ; **Delete** shows the statements, asks for the name and can remove the schemas of its tables too. **Bulk Schema Creation** generates the schema of every table at once ; **Compact Database** reclaims space on MongoDB. | | Tables | The tables or collections, with their [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html), their indexes and their [settings](https://docs.apimaker.dev/v4/docs/settings/collectionSettings.html). **+** opens the [table designer](https://docs.apimaker.dev/v4/docs/schema/table-designer.html#table-designer) ; **Run a script** its editor of scripts ; **Edit** opens the designer on the table chosen, every field filled ([below](https://docs.apimaker.dev/v4/docs/schema/table-designer.html#edit-table)) ; **Delete** shows the statement, asks for the name and can remove the schema of the table too. A sync mark appears when a table and its schema differ : click to [settle it](https://docs.apimaker.dev/v4/docs/schema/table-designer.html#schema-sync). | | APIs | The [operations of the table](https://docs.apimaker.dev/v4/docs/apis-all/overview.html#the-operations-of-a-table), each with its [settings](https://docs.apimaker.dev/v4/docs/settings/apiSettings.html) and **Test API**, which opens the [API testing page](https://docs.apimaker.dev/v4/docs/features/developer-tools.html#api-testing-page) on it. | - **Connection String Sandbox** : another string for the code which runs in the sandbox, when the sandbox reaches the database through another address. Empty, the main one is used. - **Is Multi Tenant Structure Instance** and **Connection String Multi Tenant** make a [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html) instance ; **Select tenant** then loads the databases of one tenant. ## The table designer **Designer** makes one table, column by column, with the types of its database : every type MySQL, MariaDB, PostgreSQL, SQL Server, Oracle or MongoDB has, each with what it takes (a length, `MAX`, a precision and a scale, the values of an `ENUM`). A column is a part of the primary key, not `NULL`, unique, numbered by the database (`AUTO_INCREMENT`, an identity), has a default value or expression, a comment, a collation, a check, or is computed. The table has its indexes, foreign keys, checks and options (the engine and character set of MySQL, a tablespace, `UNLOGGED`). A collection of MongoDB checks its fields with `$jsonSchema` (type, required, limits, pattern, values), and can be capped, a time series or clustered. **Quick add** puts in the columns most tables have : a key, a UUID key, created and updated times, an email. The server writes the script of the design while it is designed, on the right, and says what its database would refuse at the field it is about. **Create table** runs that script ; **Edit as script** takes it to the editor. **Script** runs any number of statements : tables made at once, with their keys, procedures, data. Statements are split the way the client of the database splits them : at `;`, at a line `GO` (SQL Server), `DELIMITER $$` (MySQL, MariaDB), a line `/` after a block of PL/SQL (Oracle) ; on MongoDB `db.createCollection(…)`, `db.users.createIndex(…)`, `insertOne`, `insertMany`, `db.runCommand(…)`. Each statement says what became of it : its time, its rows, what the database said, and an error at its line. **Stop on error** leaves out the statements after one which failed ; **All or nothing** (PostgreSQL, SQL Server) runs the script in one transaction, so a script which fails changes nothing. A script which drops, truncates or deletes asks first. Drafts of the designer and of the editor are kept in the browser. ## Editing a table there is **Edit** reads the table from its database : its columns with their types, sizes, defaults, identities and computed expressions, its primary key, unique keys, indexes, foreign keys, checks, comments and options (on MongoDB the fields of its validator, its indexes and its options). Change anything the designer shows : rename the table or a column, change a type, add or remove columns, keys, indexes and checks. Each column says what becomes of it (**new**, **edited**, **was** its old name) ; a column removed is listed under the columns, with **Put back**. **Reset** goes back to the table as it is. The **SQL** tab lists the changes and the statements which make them, written the way each database takes them : - MySQL and MariaDB : one `ALTER TABLE`, which also puts columns in their new order (`AFTER`) ; `RENAME TABLE` ; foreign keys removed first and added last. - PostgreSQL and SQL Server : a statement per change, all in one transaction : the whole is made or nothing. SQL Server takes away the defaults, keys and indexes of a column before it changes the column, and makes them again. - Oracle : `MODIFY` with only what changed. - MongoDB : `collMod` with the new validator, `renameCollection`, and the documents follow their fields : a field renamed is renamed (`$rename`), a field removed is removed (`$unset`) in every document. A column computed from a column whose type changes is made again around it. A change which can lose data, or fail on the rows there are (a column removed, a narrower type, `NOT NULL` without a default), is marked, and **Apply changes** asks first. If the table changed on the server since it was opened, the designer says so before anything runs. What the designer does not show (an index of expressions, a check which is not enforced, a validator written by hand) is listed at the top and left as it is. ## The schema follows its table Once a table is changed, its [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) is changed with it : the designer shows the code of the schema as it is and as it will be, side by side. Only what the table changed is changed : a property added, removed or renamed with its column, and inside a property only `__type`, `isPrimaryKey`, `isAutoIncrementByDB`, `validations.required` and `validations.maxLength`. Your own validations, conversions and comments stay. **Update the schema** saves it ; **Remember my choice** updates schemas without asking from then on, or never. A table made in the designer is offered a new schema. The list of tables marks a table whose schema says something else than the table : a column without a property, a property without a column, another type, required or not, another length, key or numbering. A click lists the differences, field by field, and settles them either way : - **The schema follows the table** : the code of the schema is changed for the fields chosen, shown before it is saved. - **The table follows the schema** : the designer opens on the table with the columns the schema asks for filled in, and shows the statements before they run. A schema whose table is gone is marked too, and can be removed. ## Recorded for every environment What the designer, the editor of scripts, **Delete**, the form of a database and the index forms do can be recorded into a [migration script](https://docs.apimaker.dev/v4/docs/features/database-migration.html#recorded-changes) : the pill **REC** in their footers says where the change goes (a script of the account, a new one, or **Don't record**), and the list of tables shows it in its header. The statements which ran and changed something are added to the script ; every environment which deploys the project runs it, and makes each change once, in its order. ## Editing a database **Edit** reads a database with its options. PostgreSQL and SQL Server rename a database (`ALTER DATABASE … RENAME` / `MODIFY NAME`) ; MySQL and MariaDB make the new one and move every table into it (`RENAME TABLE`) ; MongoDB moves every collection (`renameCollection`). MySQL and MariaDB change the character set and the collation, PostgreSQL the owner, SQL Server the collation, Oracle the password, the tablespace and the quota of the user. The schemas of the tables follow a database renamed. **Delete** removes a database, closing the connections to it first when asked (`pg_terminate_backend` of its sessions on every PostgreSQL version, `SINGLE_USER`, `CASCADE` of an Oracle user). ## Related - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) · [Database migration](https://docs.apimaker.dev/v4/docs/features/database-migration.html) · [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.html) --- # Data types > Every column type of MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server and Oracle through the APIs of API Maker - vectors, JSON, bytes, shapes, intervals and arrays as plain JSON, written and read the same way by generated APIs, schema APIs and g.sys.db in every language. Source: https://docs.apimaker.dev/v4/docs/schema/data-types.html Every column type of the six databases goes through API Maker as plain JSON. A vector is a list of numbers, a JSON column holds any JSON value, bytes are a `Buffer` object, a shape is GeoJSON or the form the driver of the database reads it in. The same values work in the [generated APIs](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-save-single-or-multiple-api.html#without-a-schema), the [schema APIs](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-save-single-or-multiple-api.html) and `g.sys.db` from every language : save, get by id, update, query. | | | |---|---| | Vectors | `VECTOR` of MySQL 9, MariaDB 11.7, SQL Server 2025 and Oracle 23ai, `vector` and `halfvec` of pgvector : a list of numbers. | | JSON | `JSON` of MySQL, SQL Server 2025 and Oracle 21c+, `json` and `jsonb` of PostgreSQL : objects, lists and plain values. | | Bytes | `{ "type": "Buffer", "data": [1, 2, 3] }` both ways, or its JSON text. | | Shapes | GeoJSON in, the form of the driver out. | | Schemas | A vector column is `[EType.number]`, a JSON column `EType.string` that takes objects and lists. | ## What a value looks like | Kind | Send | Read back | |---|---|---| | Vector | `[0.12, 0.5, 0.33]`, or the text `"[0.12, 0.5, 0.33]"` | `[0.12, 0.5, 0.33]` | | JSON | any JSON value : `{ "color": "blue" }`, `[1, "two"]` | the same value (MariaDB : its text, see below) | | Bytes | `{ "type": "Buffer", "data": [137, 80] }` | `{ "type": "Buffer", "data": [137, 80] }` | | Shape (MySQL, MariaDB) | GeoJSON, or the form it is read in | `{ "x": 1, "y": 2 }` for a point, lists of points for lines and polygons | | Shape (SQL Server) | GeoJSON, or WKT : `"POINT (3 4)"` | the object `mssql` reads : `srid`, `points`, `figures`, `shapes` | | Interval (PostgreSQL) | `{ "days": 1, "hours": 2 }`, or `"3 days 04:05:06"` | `{ "days": 1, "hours": 2 }` | | Interval (Oracle) | `{ "days": 1, "hours": 2 }`, `{ "years": 1, "months": 2 }`, or `"+3-04"` | the same object, every part named | | Array (PostgreSQL) | `[1, 2, 3]`, `[[1, 2], [3, 4]]`, `[{ "a": 1 }]` for `jsonb[]` | the same list | Numbers in a vector are floats of 4 bytes in most databases : `0.1` comes back as `0.10000000149011612`. Use `FLOAT64` vectors of Oracle when every digit counts. ## By database ### MongoDB - Lists and objects are stored as they are. - A BSON vector (binary data of subtype 9, as the vector search of Atlas stores it) is read as its numbers : floats, signed bytes, or bits as `0` and `1`. ### MySQL and MariaDB | Type | Read as | |---|---| | `VECTOR(n)` | list of numbers | | `JSON` | the JSON value on MySQL ; the text on MariaDB, where `JSON` is a `LONGTEXT` | | `BINARY`, `VARBINARY`, `BLOB` (all sizes), `BIT` | `Buffer` | | `GEOMETRY`, `POINT`, `LINESTRING`, `POLYGON`, the multi kinds, `GEOMETRYCOLLECTION` | points `{ x, y }`, lines and polygons as lists of them | | `DECIMAL` | text, every digit kept | | `BOOLEAN` | `1` or `0` (it is a `TINYINT`) | - A point of the system 4326 is written from GeoJSON as longitude, then latitude : `{ "type": "Point", "coordinates": [20, 10] }` is read as `{ "x": 20, "y": 10 }`. - MariaDB `UUID`, `INET4` and `INET6` are texts. ### PostgreSQL | Type | Read as | |---|---| | `json`, `jsonb` | the JSON value | | arrays (`integer[]`, `text[]`, `jsonb[]`, `bytea[]`, `vector(2)[]`…) | lists | | `vector`, `halfvec` (pgvector) | list of numbers | | `sparsevec` (pgvector) | `{ "dimensions": 5, "indices": [0, 2], "values": [1.5, 2] }`, places counted from 0 | | `bytea` | `Buffer` | | `interval` | `{ "days", "hours", "minutes", "seconds", "milliseconds" }`, the parts it has | | `point`, `circle` | `{ "x", "y" }`, `{ "x", "y", "radius" }` | | `line`, `lseg`, `box`, `path`, `polygon`, ranges, `tsvector`, `inet`, `uuid`, `xml`… | the text of PostgreSQL | - A `sparsevec` is written from all of its numbers (`[1.5, 0, 2, 0, 0]`), from the object it is read as, or from its text `{1:1.5,3:2}/5`. ### SQL Server | Type | Read as | |---|---| | `json` (2025) | the JSON value | | `vector(n)` (2025) | list of numbers | | `binary`, `varbinary`, `image` | `Buffer` | | `geometry`, `geography` | the object `mssql` reads | | `bigint` | text : a number of JavaScript does not hold every `bigint` | | `hierarchyid` | `Buffer` | - Without a schema, a list or an object sent to an `nvarchar` column is stored as its JSON text. - A `timestamp` (`rowversion`) is made by the server : leave it out of what you send. ### Oracle | Type | Read as | |---|---| | `JSON` (21c+) | the JSON value | | `VECTOR` (23ai), `FLOAT32`, `FLOAT64`, `INT8` | list of numbers | | `BOOLEAN` (23ai) | `true`, `false` | | `RAW`, `BLOB` | `Buffer` | | `INTERVAL DAY TO SECOND` | `{ "days", "hours", "minutes", "seconds", "fseconds" }`, `fseconds` in nanoseconds | | `INTERVAL YEAR TO MONTH` | `{ "years", "months" }` | | `CLOB`, `NCLOB`, `LONG`, `XMLTYPE` | text | - A JSON text longer than 32767 bytes is sent as a `CLOB`, so a large document fits. - The client libraries of Oracle before 23.4 send vectors as text : API Maker reads that text into numbers all the same. - `BFILE` is not read nor written. ## Schemas [Get schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) makes a field of each column : | Column | Field | Takes | |---|---|---| | Vector | `embedding: [EType.number]` | a list of numbers | | JSON | `details: EType.string` | objects and lists too : they are stored as their JSON, and read back as JSON | | Bytes | `photo: EType.string` | a `Buffer` object | | Shape, interval | `place: EType.string` | GeoJSON, an interval object, or their texts | | PostgreSQL array | `tags: [EType.string]` | a list | - Bytes, JSON, intervals and the large objects of Oracle (`CLOB`, `XMLTYPE`) get no `maxLength` : the length their catalog gives counts what the row stores, not characters of what you send. The database still refuses what does not fit. - A list given to a text column which is not a JSON column is still refused : ` should be array in schema as data is in array.` - Schemas made before these types were supported keep their old fields. Generate them again, or change the field by hand. ## Save, read and update over HTTP **A row of MySQL with a vector, JSON and bytes** ```text POST /api/schema/admin/mysql_8/inventory/products/save-single-or-multiple { "id": 1, "name": "Mug", "embedding": [0.12, 0.5, 0.33], "details": { "color": "blue", "sizes": [250, 350] }, "photo": { "type": "Buffer", "data": [137, 80, 78, 71] } } GET /api/schema/admin/mysql_8/inventory/products/get-by-id/1 → { "id": 1, "name": "Mug", "embedding": [0.12, 0.5, 0.33], "details": { "color": "blue", "sizes": [250, 350] }, "photo": { "type": "Buffer", "data": [137, 80, 78, 71] } } PUT /api/schema/admin/mysql_8/inventory/products/update-by-id/1 { "embedding": [0.2, 0.1, 0.9], "details": ["clearance", 2026] } ``` The [generated APIs](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-save-single-or-multiple-api.html#without-a-schema) take the same body at `/api/gen/admin/mysql_8/inventory/products/…`. ## From your code `g.sys.db` sends the same values in every language : lists, maps and the `Buffer` object as a plain map. See [save single or multiple](https://docs.apimaker.dev/v4/examples/sys/db/saveSingleOrMultiple.html#save-vectors-json-and-bytes) for the code of all eight languages. ## Related - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) · [Connection strings](https://docs.apimaker.dev/v4/docs/Database-connection-string/mysql-connection-strings.html) · [Save single or multiple](https://docs.apimaker.dev/v4/examples/sys/db/saveSingleOrMultiple.html) --- # Auto Increment > Sequential numbers on every database, MongoDB included - isAutoIncrementByAM in the schema, start and step, how the next value is chosen, the Auto Increments page, and what travels with Git. Source: https://docs.apimaker.dev/v4/docs/features/auto-increment.html MongoDB has no auto increment column. API Maker gives one to every database : mark a number field with `isAutoIncrementByAM` and every row saved through the schema APIs gets the next number. | | | |---|---| | Schema | `order_no: { __type: EType.number, isAutoIncrementByAM: { start: 1000, step: 5 } }` ; `true` means start `1`, step `1`. | | Counter | One per field, kept in the internal Redis of API Maker, shared by every server. | | Page | `Database → Auto Increments` : every counter of the account, with its last value, which you can edit. | | Git | The counters of a branch are written to `Auto Increments/auto_increments_.yml` in the repository. | ## How the next value is chosen 1. The counter of the field is increased by `step`. This is atomic : two servers saving at the same time get two different numbers. 2. When the result is not above `start` (a new counter, or a counter reset below `start`), API Maker reads the highest value the table already has : the next value is that maximum plus `step`, and at least `start`. 3. The value is written in the row. A value sent in the payload is replaced. | Start | Step | Counter | Highest in table | Next value | |---|---|---|---|---| | 1000 | 5 | 1020 | | 1025 | | 1000 | 5 | none | 1100 | 1105 | | 1000 | 5 | none | empty table | 1000 | | 1 | 1 | none | empty table | 1 | - Rows inserted outside API Maker are picked up the next time the counter starts from scratch ; edit the counter on the Auto Increments page to realign it right away. - `isAutoGenerateByAM` on the same field wins, and `isAutoIncrementByDB` disables it : let the database count when it can. - Like a database sequence, the numbers can have gaps : a save refused after its number was counted (a value of another field that is not valid, for example) keeps that number. ## The Auto Increments page - Lists the counters of the environment : instance, database, table, field path and the last generated value. - **Edit** sets the last value ; the next row gets it plus `step`. The dialog shows the values the next number will be the maximum of. - **Delete** removes a counter : the next save reads the highest value of the table again. ## With Git - A pull writes the counters of the branch into Redis when they are missing there ; a counter which exists on the server is never replaced by the one of Git. - A new environment therefore starts from the counters of the branch, and a running one keeps its own. ## Related - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html#5-keys-and-generated-values) · [Git integration](https://docs.apimaker.dev/v4/docs/Git/git.html) --- # Optimistic Concurrency Control > Stop lost updates with one schema flag - isConcurrencyControlField makes update by id and replace by id compare the version sent with the version stored, and refuse a stale write with a 400. Source: https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.html Two people open the same row. Both save. Without a check the second save silently erases the first one. With a version field the second save is refused, and the app can reload and ask. | | | |---|---| | Schema | One field with `isConcurrencyControlField: true`, usually a number stamped by a `conversionFun`. | | Checked by | [Update by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.html) and [replace by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-replace-by-id-api.html) of the schema APIs. Not update many. | | Refused with | `400` : `Concurrency version mismatch in 'version'. This row/document is already updated.` | | From code | `g.sys.db.updateById(…)` skips the check unless you pass `skipConcurrencyControl: false`. | ## 1. Add the version field ```typescript import { EType, ISchemaType, ISchemaProperty } from 'types'; const schema: ISchemaType = { _id: EType.objectId, name: EType.string, version: { __type: EType.number, conversions: { conversionFun: () => new Date().getTime() }, // a new version on every save and update isConcurrencyControlField: true, }, }; module.exports = { schema }; ``` ## 2. Send it back with every update **Read, then update with the version you read** ```text GET /api/schema/admin/mongodb/shop/products/get-by-id/68d...a1 → { "name": "Mug", "price": 20, "version": 1727683200850 } PUT /api/schema/admin/mongodb/shop/products/update-by-id/68d...a1 { "price": 22, "version": 1727683200850 } → 200, the row now carries a new version ``` - The value sent must equal the value stored. Another write in between changed it : the update answers `400` with the message above, and nothing is written. Reload the row and try again with its new version. - The field is mandatory in the payload once the row has a version : an update without it is refused the same way. ## From your code - Calls made with `g.sys.db` skip the check by default, so a scheduler or an event can update rows without carrying versions. Pass `skipConcurrencyControl: false` in the parameters to enforce it there too. - The [website](https://apimaker.dev/schema-validation) animates the whole exchange. ## Related - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) · [Update by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.html) · [Error codes](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html) --- # Multi-Tenant APIs > One set of APIs for all your customers, each with a database of its own. Keep tenants in a table, name the tenant on every request, and give each tenant's users tokens that work for their tenant only. Source: https://docs.apimaker.dev/v4/docs/features/multi-tenant.html | | | |---|---| | One instance | Serves every tenant with the same APIs, schemas, hooks and permissions. | | One database per tenant | Of the type of the instance ; the connection string lives in a row of a tenants table. | | Name the tenant | `crm::acme` in the path, or the header `x-am-tenant-username: acme`. | | Tokens | A person of a tenant gets a token which works for that tenant only. | - A multi-tenant instance serves many customers (tenants) with one set of APIs. Every tenant has its own database, of the same type as the instance. - The APIs, schemas, hooks, settings and permissions of the instance are defined once and serve every tenant. Customers share the servers and the APIs, not the databases. - The instance keeps its own connection string. That database is the structure database: a request without a tenant works on it. - Supported for MongoDB, MySQL, MariaDB, SQL Server, PostgreSQL and Oracle instances. > Diagram : Multi-tenant : one instance, one database per tenant, a pool per tenant > > A request names its tenant, crm::acme in the path or the header x-am-tenant-username. On the first request of the tenant, API Maker reads its row in the tenants table, opens a connection pool for it, and the request runs on the database of the tenant. The next requests reuse the pool. A request without a tenant runs on the structure database of the instance. ## Example: a CRM with two customers - A CRM is sold to two companies, Acme and Globex. Each one gets its own PostgreSQL database: `crm_acme` and `crm_globex`. - One instance named `crm` serves both. Its own connection string points to `crm`, the structure database, which holds the tables every tenant has: `customers`, `orders`, `users`. - The tenants live in a table `tenants` of another instance, `catalog`, one row per customer. ## 1. Keep your tenants in a table - Use a table of any instance, with one row per tenant: a username column and a connection string column. - The table needs an active schema in API Maker. - To keep connection strings encrypted, add `conversions.encryption` to that column in its schema, and save the rows through the APIs of the table. API Maker decrypts it with the default secret when it connects. - For Oracle tenants, add columns for the username and the password of each tenant, and if a tenant connects with a privilege, a column for it: `SYSDBA`, `SYSOPER`, `SYSASM`, `SYSBACKUP`, `SYSDG`, `SYSKM` or `SYSRAC`. A tenant without a privilege connects as a plain user of its database. ```json [ { "username": "acme", "connection_string": "postgresql://crm:secret@db-1:5432/crm_acme", "active": true }, { "username": "globex", "connection_string": "postgresql://crm:secret@db-2:5432/crm_globex", "active": true } ] ``` - Tenants can use different database servers: here Acme is on `db-1` and Globex on `db-2`. ## 2. Point the default secret to that table ```typescript import * as T from 'types'; let Secret: T.ISecretType | any = { // ... common and your other keys multiTenant: { crm: { instanceName: 'catalog', // instance of the tenants table databaseName: 'public', collectionName: 'tenants', usernameColumn: 'username', connectionStringColumn: 'connection_string', find: { active: true }, // optional, added to the tenant lookup // Oracle only // oracleDBUsernameColumn: 'db_username', // oracleDBPasswordColumn: 'db_password', // oracleDBPrivilegeColumn: 'db_privilege', // optional : SYSDBA, SYSOPER... empty for a plain user }, }, }; module.exports = Secret; ``` - `instanceName`, `databaseName`, `collectionName`, `usernameColumn` and `connectionStringColumn` are required. A request of the instance fails with an error naming the missing keys otherwise. - A tenant must match its username **and** the `find`. With `find: { active: true }`, setting `active` to `false` suspends a tenant. One table can also keep the tenants of several instances apart, for example with `find: { db_kind: 'pg' }`. - Each multi-tenant instance chooses its own entry, so two instances can use two tenants tables. - A request never names the entry: the instance knows it. The tenant is all a request names. ## 3. Mark the instance - Open the instance and check **Is Multi Tenant Structure Instance**. - In **Connection String Multi Tenant**, choose the entry of the secret, for example `multiTenant.crm`. ## 4. Name the tenant in every request - In the path, after the instance name, with two colons. ```text GET /api/gen/admin/crm::acme/crm/customers # the customers of Acme, from crm_acme GET /api/gen/admin/crm::globex/crm/customers # the customers of Globex, from crm_globex GET /api/gen/admin/crm/crm/customers # no tenant: the structure database ``` - Or with the [x-am-tenant-username](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html#x-am-tenant-username) header. ```text GET /api/gen/admin/crm/crm/customers x-am-tenant-username: acme ``` - The rules: - The instance looks the tenant up in its own tenants table, the entry chosen as its **Connection String Multi Tenant**. One header therefore serves every multi-tenant instance, even instances with different tenants tables. - An empty header names no tenant: the structure database. - When the path and the header both name a tenant, the tenant of the path is used. - A tenant the table does not have, or which the `find` leaves out: 400, `Unable to find tenant with username 'acme'.` - `crm::` or `::acme`: 400, `Please provide valid multi tenant instance name.` - A tenant named for an instance which is not multi-tenant changes nothing: that instance has one database for every tenant. ## Custom APIs - Call a custom API with the tenant header. It reads the tenant in `g.req.params`, and the APIs it calls with `g.sys.db` and `g.sys.system` work on the database of that tenant: the header is passed on to them. Each instance they call finds the tenant in its own tenants table. ```text GET /api/custom-api/admin/customers-report x-am-tenant-username: acme ``` **TypeScript** **The tenant of the request, and its data** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const tenant = g.req.params.tenantUsername; // 'acme', undefined without the header // The database of acme : no need to name the tenant. const customers = await g.sys.db.getAll({ instance: 'crm', database: 'crm', collection: 'customers' }); const orders = await g.sys.db.gen.queryGen({ instance: 'crm', database: 'crm', collection: 'orders', find: { status: 'paid' }, }); return { tenant, customers, orders }; } module.exports = main; ``` - Another tenant, or the structure database, on purpose: **TypeScript** **Name the tenant of one call** ```typescript // In the instance name : the tenant of the name wins over the tenant of the request. const customersOfGlobex = await g.sys.db.getAll({ instance: 'crm::globex', database: 'crm', collection: 'customers' }); // With the header of the call. const ordersOfGlobex = await g.sys.db.count({ instance: 'crm', database: 'crm', collection: 'orders', headers: { 'x-am-tenant-username': 'globex' }, }); // An empty header : the structure database. const templates = await g.sys.db.getAll({ instance: 'crm', database: 'crm', collection: 'templates', headers: { 'x-am-tenant-username': '' }, }); ``` - Queries and table meta run in the database of the tenant too: **TypeScript** **SQL of the tenant of the request** ```typescript const rows = await g.sys.system.executeQuery({ instance: 'crm', database: 'crm', query: 'select status, count(*) as orders from orders group by status', }); const columns = await g.sys.system.getTableMeta({ instance: 'crm', database: 'crm', table: 'orders' }); // Another tenant : name it in the instance. const rowsOfGlobex = await g.sys.system.executeQuery({ instance: 'crm::globex', database: 'crm', query: 'select count(*) from orders' }); ``` - A custom API with `enableCaching` keeps its answers per tenant. Reset them for one tenant: **TypeScript** **Reset the cache of a custom API for one tenant** ```typescript await g.sys.cache.resetCacheCustomApis({ name: 'customers-report', tenantUsername: 'acme' }); // without tenantUsername : every tenant ``` - A custom API whose `authProviders` name a token generator on a users table of the instance needs a token of the tenant of the request, see [Users of a tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html#users-of-a-tenant). ## Hooks - The pre and post hooks of a request of a tenant read it in `g.req.params.tenantUsername`. - The APIs they call work on the database of that tenant, whether the request named the tenant in the path or with the header. **TypeScript** **Post hook : an audit row in the database of the tenant** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { await g.sys.db.saveSingleOrMultiple({ instance: 'crm', database: 'crm', collection: 'audit_logs', saveData: { action: 'customer saved', tenant: g.req.params.tenantUsername }, }); } module.exports = main; ``` ## What API Maker does - Reads the row of the tenant from the tenants table, decrypts its connection string if needed, and opens a connection pool for that tenant (for PostgreSQL, one per tenant and database). The next requests of the tenant reuse the pool. - A name the tenants table does not know opens no pool. - A new tenant needs no restart and no deployment: its first request reads its row. Onboarding a customer is creating its database and adding one row to the tenants table. - Logs record the tenant of each call. - The databases panel and the API testing page let you pick a tenant to work with its data. ## Caching - The cached answers of a multi-tenant instance are kept per tenant: a tenant never gets the cached answer of another tenant, nor of the structure database. - A request naming its tenant in the path and one naming it with the header share their cached answers. - A write for one tenant drops the cached answers of that tenant. A write to the structure database drops those of every tenant. - The tables of an instance which is not multi-tenant are read by every tenant: a write made for any tenant drops the cached answers of all of them. - Custom APIs with caching keep their answers per tenant as well. A write to the database of a tenant drops the cached answers of the custom APIs reading that table, for that tenant. - To reset the cache of a table by hand: **TypeScript** **Reset the cache of a table** ```typescript await g.sys.cache.resetCacheDB({ instance: 'crm::acme', database: 'crm', collection: 'customers' }); // acme only await g.sys.cache.resetCacheDB({ instance: 'crm', database: 'crm', collection: 'customers' }); // the structure database and every tenant ``` ## Users of a tenant - Users can live in a users table inside each tenant database, like `crm.users` above. - Add a DB token generator on that table of the multi-tenant instance, on the **Token Generators** page, for example `crm_users_token`. - A user gets a token with the [token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html) and the tenant header: ```text POST /api/system-api/admin/token x-am-tenant-username: acme { "name": "crm_users_token", "u": "alice@acme.com", "p": "PASSWORD" } ``` - API Maker reads Alice from the users table of `crm_acme`, and the token it returns is made for `acme`: - Requests of `acme` accept it, as long as Alice is in the users table of `crm_acme`. - A request for `globex` is refused with 401: `Tenant from x-am-user-authorization and x-am-tenant-username should be same.` Even when `globex` has a user with the same username and password. - A request without a tenant, on the structure database, is refused with 401. - A refreshed token keeps the tenant, whatever tenant the refresh call names. - A token made without a tenant, or from a users table in another instance, is not tied to a tenant: it is checked against the users table of the tenant each request names. To keep such a caller to its own tenant, compare `g.req.params.tenantUsername` with the tenant of the caller in a pre hook. ## After a tenant moves - When the connection string of a tenant changes, call the **Multi tenant instance updated** system API. Every server drops the pools of that tenant, for PostgreSQL the pools of all its databases. The cached answers of the instance and of the tenants table are cleared, so the next request of the tenant reads its row again and connects to the new database. - Until then, the servers keep using the pools they have. **TypeScript** ```typescript await g.sys.system.multiTenantInstanceUpdated({ instanceName: 'crm', username: 'acme' }); ``` - It also accepts an array of `{ instanceName, username }`, and answers each item: `{ success: true, data: true }`, or `success: false` with the error: an unknown instance (404) or an instance which is not multi-tenant (400). ```text POST /api/system-api/admin/multi-tenant-instance-updated [{ "instanceName": "crm", "username": "acme" }, { "instanceName": "billing", "username": "acme" }] ``` ## Good to know - A schema change must run on every tenant database: plan migrations for all of them. - Each API Maker process keeps one pool per active tenant. Size the connection limits of your database servers for it. (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/features/multi-tenant.md) --- # Database Migration Scripts > Migration scripts in API Maker - code in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin with the same g as a custom API, run once per environment in order after a Git pull or a deployment hook, or by hand from the Database Migration page, with the Index Maker script among them, and the changes of tables recorded from the admin panel. Source: https://docs.apimaker.dev/v4/docs/features/database-migration.html A migration script changes the structure or the data of your databases : add a column, backfill a value, create an index. It is code of the account, kept in Git, and API Maker runs each script **once per environment**, in order. | | | |---|---| | Page | `Database → Database Migration` : name, label, order, timeout in minutes, active, and the code. | | Runs | After a Git pull and after a [deployment hook](https://docs.apimaker.dev/v4/docs/Git/git.html#deployment-hooks) : the active scripts not yet executed on this environment, by `order`. Or by hand : **Execute script**, **Execute Selected Migration**. | | Remembers | The names of the scripts executed, per environment. **Mark as executed** skips one without running it ; a script already run shows **Script executed**. | | Code | The function `main` of any [language](https://docs.apimaker.dev/v4/docs/languages/index.html), with the same `g` as a custom API. Test it on the [API testing page](https://docs.apimaker.dev/v4/docs/features/developer-tools.html#api-testing-page). | ## A script **TypeScript** **Add a column once** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const meta = await g.sys.system.getTableMeta({ instance: 'mysql8', database: 'inventory', table: 'order_transactions' }); const hasDescription = meta.some(column => column.name === 'description'); if (!hasDescription) { await g.sys.system.executeQuery({ instance: 'mysql8', query: 'ALTER TABLE `inventory`.`order_transactions` ADD COLUMN `description` varchar(255) NULL AFTER `qty`', }); } return { hasDescription }; } module.exports = main; ``` - [Get table meta](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-table-meta-api.html) tells what is there, [execute plain query](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-execute-plain-query-api.html) runs any SQL or MongoDB command, [create indexes](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.html) adds indexes the same way on every database. Data moves with `g.sys.db`. - Write scripts which can run twice without harm, like the one above : a script which fails half way is run again after the fix. ## Order and timeout - `order` sorts the scripts ; the pull runs them from the lowest. Use it for a script which needs the column another one adds. - `Timeout in minutes` stops a script which runs too long (`10` when empty) : after a pull, after a deployment hook, with **Execute script** and on the API testing page alike. A script in another language than TypeScript runs in the sandbox of its language. - A script which throws stops with its error, shown by **View error** on its row ; the next scripts still run. ## Recorded from the panel What you do to tables, collections, indexes and databases in the panel can go into a migration script as you do it : every other environment then makes the same change when it deploys. No SQL to copy, nothing forgotten on staging. | | | |---|---| | Where | The pill **REC** of the [table designer](https://docs.apimaker.dev/v4/docs/schema/table-designer.html) (**Create**, **Apply changes**, **Run**), of **Delete** of a table or a database, of the form of a database, of the index forms. It says where the change goes : a migration script of the account, a **New script** you name (or the name it suggests, `create_orders`), or **Don't record**. The choice stays for the next changes, and a change recorded into a new script makes it the script of the next ones. | | What | The statements which ran and changed something, as they ran : a `SELECT` or a `find` of the editor of scripts is not recorded. A database made, changed or removed is kept as its design and written again for each environment. An index is kept in the words of [create indexes](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.html). | | Runs | The script runs like any other : after a pull, after a deployment hook, or by hand. Each change runs **once per environment**, in its order : the changes an environment has already (the ones recorded there too) are skipped, so the list can grow and the script can run again and again. A pull which changes the list marks the script as not executed, so the new changes run on the next deploy. | | Names | Databases are named as your account names them : the [database name masking](https://docs.apimaker.dev/v4/docs/features/team-work.html#mask-database-names) of each environment finds its own database. | ### The script A script the panel makes is TypeScript : the list of the changes between two markers, and one call which makes them. Each statement is written between backticks, as it ran : it reads as SQL, and the editor colours it as the SQL of its database. **create_orders : recorded in the panel** ```typescript import * as T from 'types'; /** * Database changes recorded in the admin panel (format 1) : "create_orders". * … */ // const CHANGES: T.IRecordedChange[] = [ { "id": "20261022-101502-a3f9c1", "operation": "CREATE_TABLE", "summary": "Create table orders : 3 columns", "instance": "mysql8", "database": "inventory", "table": "orders", "statements": [ `CREATE TABLE \`orders\` ( \`id\` INT NOT NULL AUTO_INCREMENT, \`status\` VARCHAR(20) NOT NULL DEFAULT 'NEW', \`total\` DECIMAL(10, 2) NULL, PRIMARY KEY (\`id\`) )` ], "recordedAt": "2026-10-22T10:15:02.123Z", "recordedBy": "admin@admin.com" }, { "id": "20261022-101733-07be42", "operation": "ALTER_TABLE", "summary": "Change table orders : add note", "instance": "mysql8", "database": "inventory", "table": "orders", "statements": [ `ALTER TABLE \`orders\` ADD COLUMN \`note\` TEXT NULL` ], "recordedAt": "2026-10-22T10:17:33.481Z", "recordedBy": "admin@admin.com" } ]; // async function main(g: T.IAMGlobal) { // The changes this server has not made yet run now, in their order. One which fails throws : the script is then // not marked as executed, and its next run starts again from that change. const results = await g.sys.system.applyRecordedChanges({ changes: CHANGES }); return results.map(result => `${result.status} : ${result.summary}`); } module.exports = main; ``` - A change : `id` (the same in every environment), `operation` (`CREATE_TABLE`, `ALTER_TABLE`, `DROP_TABLE`, `RUN_SCRIPT`, `CREATE_DATABASE`, `ALTER_DATABASE`, `DROP_DATABASE`, `CREATE_INDEX`, `DROP_INDEX`), `summary`, `instance` (and `tenant`), `database`, `table`, `statements`, `atomic` (one transaction, on PostgreSQL and SQL Server), `design` and `original` for a database, `index` for an index. - Edit the list if you need to : it is JSON, read with comments and trailing commas, with each statement between backticks. Remove a change, add one of your own, or write your own code around the call, a backfill of the new column after it for example. The panel only rewrites what is between the markers. - A change recorded into a TypeScript script you wrote adds the list above its `main` and the call as the first line of `main`. A script in another language takes no recorded change : the pill says so. #### A statement between backticks | | | |---|---| | Escapes | A statement is a text of JavaScript between backticks : write `` \` `` for a backtick of the statement (the names of MySQL and MariaDB, `` \`orders\` ``), `\\` for a backslash and `\${` for `${`. The panel writes them so ; the editor shows their backslash faint. `${…}` with code in it is refused : a statement is text. | | Indentation | The lines after the first are indented with the code around them. Before a statement runs, API Maker takes away the indentation those lines share, so what runs is what was written in the panel. A statement can also start on the line after its backtick, and end with its backtick on a line of its own. | | Before | Scripts recorded before the backticks hold a statement over several lines as the list of its lines (`["CREATE TABLE a (", " id INT", ")"]`) : they read and run as they are, and the panel writes their list between backticks the next time it records into them. | #### In the editor The **Code** tab of a script which holds recorded changes knows the list : - Each statement is coloured as the SQL of its database (MySQL and MariaDB, PostgreSQL and Oracle, SQL Server), told by how it quotes names (`` \`orders\` ``, `"orders"`, `[orders]`), and the commands of MongoDB as JavaScript. - In a statement, the tables of the database of its change and the columns of the tables it names are offered as you type, with the words of SQL. - In the list, type `change` : **New recorded change** writes a change of your own statements with a new id (the instance and the database to choose). In the `statements` of a change, **New statement** adds one. A change written by hand runs like a recorded one : once on each environment, in its order. Keep its id as it is once another environment has it. - What became of each change on this server : a bar beside the change, a note after its id (`✓ Made here`, `◷ Waits to be made here`, `✗ Failed here`), the whole of it (the error of the database) on hover, and the count on the line of `// `. ### What each environment did `applyRecordedChanges` answers one result per change, and keeps it in the environment, never in Git : | Status | | |---|---| | `APPLIED` | Made by this run. | | `ALREADY_APPLIED` | Made before : recorded here, run before, or taken as made. | | `FAILED` | The database refused it : `message` says why. The call throws, so the script is not marked as executed and its next run starts again from that change. On MySQL, MariaDB and Oracle, which commit each statement by itself, the statements of the change which ran before the failure are not run again. | | `NOT_RUN` | A change before it failed : it runs next time. | | `PENDING` | `dryRun: true` : nothing runs, the change would. | On the Database Migration page, the red count beside a script opens its **Recorded changes** : each change with what became of it on this server (made here, waiting, failed with the error of the database, taken as made), its statements coloured as their SQL with a button to copy each one (to run it by hand on a server, then take the change as made there), and : - **Take as made here** : this server has it already (made by hand) : a run here skips it. **Mark as executed** of a script does the same for all of its changes. - **Make again here** : the next run makes it on this server again. - **Take out** : the servers which did not make it yet never will. What it made here stays. - **Make N here now** : runs the script on this server. > **Good to know** > > - An Oracle user is made with a password, which would go to Git with the script : it is not recorded, the pill says so. > - A statement of the editor of scripts which names a database itself (`USE`, `` `shop`.`orders` ``) runs in that database everywhere : name the tables alone, each environment runs them in its own database. > - The [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) of a table follows it through Git, as every schema does : the script holds the changes of the database. ## The Index Maker script - `am_index_maker` is managed by [Index Maker](https://docs.apimaker.dev/v4/extensions/index_maker/introduction.html) : it holds the indexes decided by the automatic mode, the suggestions and the index form as a JSON list, runs on every deploy, and can not be deleted while Index Maker uses it. Edit the code outside the marked region as you like. ## Related - [Git integration](https://docs.apimaker.dev/v4/docs/Git/git.html) · [Execute plain query](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-execute-plain-query-api.html) · [Create indexes](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.html) · [Table designer](https://docs.apimaker.dev/v4/docs/schema/table-designer.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/features/database-migration.md) --- # SSR websites > Websites written in TSX and rendered by the API Maker server, next to your data - file routing, layouts, loaders, forms with CSRF protection, sign-in with your auth providers, row rules, page cache, UI libraries, SEO, git and templates. Source: https://docs.apimaker.dev/v4/docs/ssr/index.html A website of API Maker is a folder of files : pages written in TSX, the components and the code they share, files sent as they are, and its settings. The server renders the pages next to your data and sends HTML : the browser shows a page as it reads it, and nothing runs in it unless you add a script. A website answers at `/ssr///` of the server : the path of its account, as the addresses of its APIs have it, then its name, unique in the account. A reverse proxy shows it at the root of a domain of its own : see [deployment](https://docs.apimaker.dev/v4/docs/ssr/deployment.html). | | | |---|---| | Where | Dashboard → **SSR Websites** : a website from a template, its files, the editor, the preview, its UI libraries, its routes, its settings. [The panel](https://docs.apimaker.dev/v4/docs/ssr/editor.html). | | Pages | `pages/` : `index.tsx` is `/`, `blog/[slug].tsx` is `/blog/:slug`, a `.ts` file is an endpoint. [Routing](https://docs.apimaker.dev/v4/docs/ssr/pages-and-routing.html). | | Data | A `loader` reads on the server ; `ctx.db` reaches the tables the website lists, with the rows each visitor may reach. [Data](https://docs.apimaker.dev/v4/docs/ssr/data.html). | | Visitors | Sign-in with the auth providers of the account, `access` on a page or a layout, groups. [Authentication](https://docs.apimaker.dev/v4/docs/ssr/authentication.html). | | Forms | An `action` takes a form ; every form carries a CSRF token by itself. [Forms](https://docs.apimaker.dev/v4/docs/ssr/forms.html). | | Speed | Templates compiled when a file is saved, engines kept per website, pages kept by the server. [Cache](https://docs.apimaker.dev/v4/docs/ssr/caching.html). | | Browser | Bootstrap, Tailwind, Chart.js, htmx… from a catalog with their hashes. [UI libraries](https://docs.apimaker.dev/v4/docs/ssr/ui-libraries.html). | | Git | `src/ssr//` of the repository of the account : commit, pull, revert. [Git](https://docs.apimaker.dev/v4/docs/ssr/git.html). | ## Your first website 1. Dashboard → **SSR Websites** → **New website**. 2. Choose **Starter** : a page for every feature, each with comments which say why it is written so. 3. Name it `demo`. Its address is `/ssr//demo/` : `/ssr/acme/demo/` for the account of the path `acme`. 4. Open `pages/index.tsx`, change a word, save with Ctrl+S : the preview reloads by itself. A page : **pages/products/index.tsx** ```tsx import { formatCurrency } from 'am-ssr'; import type { PageConfig, PageContext, PageProps } from 'am-ssr'; export const config: PageConfig = { title: 'Products', // One page for every anonymous visitor, kept a minute. cache: { maxAge: 60, scope: 'public' }, }; // Runs on the server before the page renders : what it returns is `data`. export async function loader(ctx: PageContext) { return { products: await ctx.db.query({ instance: 'mongodb', database: 'shop', table: 'products', limit: 20 }), }; } export default function Products({ data }: PageProps) { return (
    {data.products.map((product) => (
  • {product.name} {formatCurrency(product.price, 'USD')}
  • ))}
); } ``` And its website lists the table it reads, in `site.config.ts` : a website reaches no table it does not name. **site.config.ts** ```ts import { defineSite } from 'am-ssr'; export default defineSite({ title: 'My shop', titleTemplate: '%s · My shop', data: { read: ['mongodb/shop/products'] }, }); ``` ## The files of a website ```text site.config.ts the settings : tables ctx.db reaches, auth providers, CSP, cache, SEO, redirects libraries.json the UI libraries the pages load in the browser pages/ _layout.tsx the HTML around every page _middleware.ts code which runs before every page and endpoint _404.tsx _error.tsx what a path no page answers shows, and what a page which failed shows index.tsx / about.tsx /about blog/[slug].tsx /blog/:slug docs/[...path].tsx /docs/a/b/c (shop)/cart.tsx /cart, with the layout of (shop)/ api/items.ts an endpoint : GET, POST, PUT, PATCH, DELETE components/ components the pages share : import Card from '@/components/Card' lib/ code the pages share data/ JSON the pages import : import posts from '@/data/posts.json' public/ files sent as they are : public/logo.png is /logo.png ``` ## What happens to a request > Diagram : A request to an SSR website : route, access, cache, render, HTML > > The path of a request to an SSR website : the server finds the website and the route, checks who may open the page before any code of it runs, sends a page kept by the cache as it was kept, else renders it : middlewares, the action of a form, the loaders of the layouts and of the page at once, the page and its layouts. Then it writes the head, the CSRF token and the nonce of the CSP, and sends the HTML. 1. The server finds the website of `/ssr//`, built for its version : its files were compiled when they were saved. 2. Redirects of the settings, files of `public/`, `/robots.txt` and `/sitemap.xml` answer first. 3. The route of the path, and who may open it : the `access` of its layouts and of the page, read from their code when they were saved. A visitor who may not is refused **before any code of the page runs**. 4. A form of an unsafe method needs the CSRF token of its visitor. 5. A page kept by the cache is sent as it was kept. 6. The render, in an engine of the website : the middlewares, the `action` for a form, the loaders of the layouts and of the page at once, the page, then each layout around it. 7. The server writes the head (title, description, Open Graph, the UI libraries), the CSRF token and the nonce of the CSP into the page, and sends it. ## Safe by default - Every value is escaped where it is written. HTML which is not is asked for by name (`raw()`). - `ctx.db` reaches the tables `site.config.ts` lists, with the row rules it gives each : `{ customer_id: '$user._id' }` keeps every query to the rows of the visitor. - Sessions are cookies sealed with AES-256-GCM, `HttpOnly`, `SameSite=Lax`, `Secure` over HTTPS. - Every form has a CSRF token ; every ` ); } ``` ## The CSP The policy of a page (`strict` by default) lets in the scripts and the style sheets of the libraries **that page** loads, by their origins : a page without Chart.js does not allow its CDN for scripts. A library which needs more says so in the catalog (Alpine.js needs `'unsafe-eval'`, Leaflet loads tiles from OpenStreetMap) ; an entry may add its own needs with `csp`. A self-hosted library comes from the website itself : the policy needs nothing for it. ## Self-hosting With `selfHost`, the panel downloads the files of the entry from jsDelivr, checks each against its hash (a file which differs is refused), and copies it into `public/_lib/@/`, with the fonts and images its style sheets name. The website then asks no CDN : nothing of your visitors goes to another company, and the website works where the CDN does not answer. Remove the folder of a library from `public/_lib` when you remove it from the list. --- # Images - SSR websites > Make the images of an SSR website smaller in one click - a whole folder or one image, JPEG with MozJPEG, WebP with libwebp, PNG without loss, a largest size, a preview with a slider, the sizes before and after, and the references updated when an image changes its format. Source: https://docs.apimaker.dev/v4/docs/ssr/images.html The images of a website are most of what its pages download. The panel makes them smaller where they are : every image of a folder in one click, or one image. - **JPEG** is written by [MozJPEG](https://github.com/mozilla/mozjpeg) : trellis quantisation, optimised tables, progressive scans — about a tenth smaller than other encoders at the same quality. - **WebP** is written by [libwebp](https://developers.google.com/speed/webp), lossy or lossless. - **PNG** keeps every pixel : an alpha channel nobody sees is dropped, greys are written as greys, an image of 256 colours or fewer gets a palette of 1, 2, 4 or 8 bits a pixel. When asked, a PNG of more colours gets a palette of 256 too, which loses a little. - Every image is made to fit a **largest size** (1920 × 1920 by default), its proportions kept ; a smaller one keeps its size. - A photo is turned the way its camera said, then its **EXIF** is gone : the camera, the date, and the GPS position nobody wants published. ## Compress images | To | Do | |---|---| | Compress a folder | Right click a folder of the tree : **Compress images…** (the number of images below it is on the right). | | Compress the whole website | **Compress every image…** in the `⋮` menu of the files, or in the menu of the empty part of the tree. | | Resize and compress one image | Right click it : **Resize & compress…** ; or open it, and **Resize & compress** in the bar above it. | | Make a new image of one | Right click it : **Save as a new image…** ; or open it, and **Save as new** in the bar above it. | The dialog lists the images with their formats and their pixels, the largest first. The settings start at the defaults ; change one and the image of the preview is made again. ### A new image The dialog of one image writes the result where the image is (**Replace the image**), or as a new file beside it (**As a new image**) : a smaller copy for a thumbnail, a WebP of a JPEG, a version for phones. The name starts as the pixels of the result (`hero-800x533`) and follows the settings until you type one ; the extension is the one of the format (`.webp`, `.jpg`, `.png`). - The original stays as it is, and no page changes : name the new image where you use it, `asset('/img/hero-800x533.webp')`. - A new image is written smaller or not : you asked for it. **Only when smaller** and the options of another format are for the original. - A name which a file of the website has already is refused : nothing is written over. - Once it is made, the new image opens in the editor, with its size, its format and its pixels. **75 %**, **50 %** and **25 %** under the largest size make the image that share of its size, its proportions kept. A width with a height of 0 makes it that wide. ### The preview The image of the preview is shown twice : its original left of the line, the result right of it. Drag the line, or move it with Left and Right. **100 %** and **200 %** show the pixels themselves : scroll to a face, a text, a gradient, and look for the difference. Above it : the size of the original, the size of the result, and how much smaller it is. Click another image of the list to see it. ### The sizes Three numbers above the list say what the images take now, what they will take, and what you save. **Estimate savings** makes every chosen image as a trial, without writing it : each row shows its size now, its new size and the gain, with a bar. The images are then written as the trial made them, without being made again. **Compress** writes them, a few at a time : each row turns green with its gain as it is written, and the new size goes down while you watch. **Stop** finishes the images under way and writes no more. ### The settings | Setting | Default | What it does | |---|---|---| | Preset | Balanced | **Balanced** : quality 80, 1920 px. **Smallest** : quality 60, 1280 px, the colours of a PNG reduced. **High quality** : quality 90, 2560 px, every colour of a JPEG kept. | | Format | Keep | Each image stays what it is. **WebP** : a quarter to a third smaller than JPEG, transparency kept, every browser shows it. **JPEG**, **PNG** : every image as that format. GIF and BMP are made smaller only as another format. | | Quality | 80 | 1 to 100, for JPEG and lossy WebP. Below 70 the difference starts to show on photos. | | Largest size | 1920 × 1920 | The box every image is made to fit ; 0 is no limit on that side. 4K, QHD, Full HD, HD and 800 in one click. | | Progressive | On | A JPEG shows coarse first, then sharp. MozJPEG writes it smaller too. | | Keep the colour profile | On | The ICC profile of a JPEG is written into the result : photos of wide colours (Display P3) stay right. | | Colour detail | 4:2:0 | How much of the colour a JPEG keeps : 4:2:0 as cameras write, 4:4:4 for text and thin coloured lines. | | Reduce to 256 colours | Off | A PNG of more colours gets a palette of 256, its errors spread over the neighbouring pixels (dithering). Often 60–80 % smaller for drawings and screenshots ; gradients may show bands. | | WebP | Auto | **Auto** : lossy for a photo ; for an image which came without loss (PNG, GIF), lossless unless that is clearly larger. **Lossy**, **Lossless** : always so. | | Effort | 4 | 0 to 6 : how hard libwebp works. 6 is a little smaller, and slower. | | Background | White | What transparent pixels become in a JPEG, which has no transparency. | | Only when smaller | On | An image the settings would not make smaller stays as it is (**small already**). | | Replace the originals | On | With another format : `hero.jpg` goes once `hero.webp` is written. Off, both stay. | | Update the references | On | With another format : the pages, the data and the style sheets which name `/hero.jpg` name `/hero.webp`. | ### Another format An image written as another format gets the extension of it : `public/img/hero.jpg` becomes `public/img/hero.webp`. The texts of the website which name it are changed to name the new file : - its address, in a page, a layout, data or anywhere : `asset('/img/hero.jpg')`, `"image": "/img/hero.jpg"` ; - its path from a style sheet or a script of `public/` : `url(img/hero.jpg)`, `url(../img/hero.jpg)` ; - its path in the website : `public/img/hero.jpg`. Only a name which stands alone is changed : `/img/hero.jpg.bak` and `/other/img/hero.jpg` are other files. The dialog says which texts changed ; they are compiled again, and the problems of the website show anything which no longer finds its image. An image whose new name is taken already (`hero.webp` is there) is left as it is. ## What is left as it is | Image | Why | |---|---| | An animation (GIF, WebP, APNG) | Made smaller, it would keep its first frame only. | | SVG | It has no pixels : it is a text. | | AVIF, TIFF, ICO | Not read here. AVIF is small already. | | A GIF or a BMP, with **Keep** | Choose WebP, PNG or JPEG for it. | | An image already small | A result which is not smaller is not written (**Only when smaller**). | ## Before you compress The originals are replaced. Commit the website on the Git page first : the [history of a file](https://docs.apimaker.dev/v4/docs/ssr/git.html#the-history-of-a-file) brings back any version which was committed, an image too. After compressing, commit again to keep the smaller images. Images are made on the server, a few at a time, on threads of their own : the APIs of the server keep the rest of its processors. An image is at most 8 MB, and at most 16 383 pixels a side. --- # SEO - SSR websites > What search engines and social networks read of the pages of an SSR website - title and title template, description, Open Graph and Twitter cards, canonical addresses, noindex, sitemap.xml, robots.txt and JSON-LD. Source: https://docs.apimaker.dev/v4/docs/ssr/seo.html A page rendered by the server is read by a search engine as people read it : the text is in the HTML. The server writes its head from its settings. ```ts export const config: PageConfig = { title: 'Pricing', // Pricing · My product with titleTemplate '%s · My product' description: 'Free to start, simple when you grow.', // and og:description image: '/og.png', // og:image (a file of public/, or an address) and a large Twitter card canonical: '/pricing', // : the whole address index: false, // }; ``` Each may be made from the data of the page : ```ts export const config: PageConfig = { title: (data) => data.post.title, description: (data) => data.post.summary, canonical: (data) => `/posts/${data.post.slug}`, }; ``` | Head | From | |---|---| | `` | `title` of the page (else of its layouts, else `title` of the website), in `titleTemplate` of `site.config.ts` | | `description`, `og:description` | `description` | | `og:title` | `title` | | `og:image`, `twitter:card` | `image`, written as a whole address | | `canonical` | `canonical`, written as a whole address : the origin, the base path, the path | | `robots` | `index: false` of the page or of a layout, or `seo.noIndex` of the website | ## More in the head `head(props)` of a page or a layout adds meta tags and links, or any JSX : ```ts export function head({ data }: PageProps<typeof loader>): Head { return { meta: [ { property: 'og:type', content: 'article' }, { property: 'article:published_time', content: data.post.date }, ], links: [{ rel: 'alternate', hreflang: 'fr', href: `/fr/posts/${data.post.slug}` }], }; } ``` ## Structured data JSON-LD is a script of the page : `serialize()` writes JSON nothing can break out of. ```tsx <script type="application/ld+json">{serialize({ '@context': 'https://schema.org', '@type': 'Product', name: data.product.name, offers: { '@type': 'Offer', price: data.product.price, priceCurrency: 'USD' }, })}</script> ``` ## sitemap.xml and robots.txt - `/sitemap.xml` lists the pages of the website whose paths take no value, but the ones which say `sitemap: false` or `index: false`, and the pages for signed-in visitors. `sitemap: { changefreq, priority }` tells more. - `/robots.txt` allows every robot (`seo.robots: 'allow'`), none (`'disallow'`), or says your text ; it names the sitemap. - A website with pages whose paths take values (posts, products) writes its own : a page or an endpoint at `pages/sitemap.xml.ts` answers in place of the one of the server. **pages/sitemap.xml.ts** ```ts import { escape } from 'am-ssr'; import type { PageContext } from 'am-ssr'; export async function GET(ctx: PageContext) { const home = ctx.site.origin + ctx.site.base; const posts = await ctx.db.query({ instance: 'mongodb', database: 'blog', table: 'posts', select: 'slug updatedAt', limit: 5000 }); const urls = posts.map((post) => `<url><loc>${escape(home + '/posts/' + encodeURIComponent(post.slug))}</loc></url>`); ctx.setHeader('content-type', 'application/xml; charset=utf-8'); return ctx.text(`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join('')}</urlset>`); } ``` ## Addresses - `seo.noIndex: true` keeps the whole website out of search engines (a staging copy). - `trailingSlash` : `/about/` goes to `/about` (`'never'`, the default), the other way (`'always'`), or both answer (`'ignore'`). - `redirects` of `site.config.ts` send old addresses to new ones (`308` unless a `status` says another ; `/old/*` takes every path below `/old`). - Behind a domain of its own, the addresses of the pages are at the root of the domain : see [deployment](https://docs.apimaker.dev/v4/docs/ssr/deployment.html). --- # Settings of a website - SSR websites > Every setting of site.config.ts of an SSR website - title, domains, auth providers, default access, the tables ctx.db reaches with their row rules, ctx.api, ctx.sys, ctx.fetch, cache, CSP and headers, SEO, redirects, trailing slashes, maintenance and limits. Source: https://docs.apimaker.dev/v4/docs/ssr/site-config.html `site.config.ts` gives the settings of a website **as values** : the server reads them when the file is saved, and decides with them before any code of a page runs. Code there (a function, a variable) is a problem the panel shows : the settings it can read are kept. **site.config.ts** ```ts import { defineSite } from 'am-ssr'; export default defineSite({ title: 'My shop', titleTemplate: '%s · My shop', description: 'Things for a calm home.', lang: 'en', domains: ['shop.example.com'], auth: { providers: ['customers'], loginPath: '/login', sessionDays: 30 }, defaultAccess: 'public', data: { read: ['mongodb/shop/products', { table: 'mongodb/shop/orders', where: { customer_id: '$user._id' }, requireUser: true }], write: [{ table: 'mongodb/shop/orders', where: { customer_id: '$user._id' }, requireUser: true, allow: ['create'] }], apiUser: 'website', allowSys: false, fetch: ['api.stripe.com'], }, cache: { maxAge: 0 }, security: { csp: 'strict', frameAncestors: ["'self'"], headers: { 'x-powered-by': 'API Maker' } }, seo: { sitemap: true, robots: 'allow', noIndex: false }, redirects: [{ from: '/old-shop/*', to: '/products', status: 301 }], trailingSlash: 'never', maintenance: false, limits: { memoryMb: 64, cpuMs: 2000, timeoutMs: 30000 }, }); ``` ## Typed settings `defineSite` takes a `SiteConfig` of `am-ssr` : in the editor of the panel, in VS Code and in WebStorm (the folder of the website in [git](https://docs.apimaker.dev/v4/docs/ssr/git.html)), every setting is completed with what it does, its default and the values the server takes, and a mistake is marked before the file is saved — a setting of another name (`defaultAcess`), a table which is no `instance/database/table`, a `loginPath` or a redirect which is no path, a status or a word the server does not know. **site.config.ts** ```ts export default defineSite({ data: { read: ['shop/products'], // ~~~~~~~~~~~~~~~ a table is 'instance/database/table' }, trailingSlash: 'sometimes', // ~~~~~~~~~~~ 'never', 'always' or 'ignore' }); ``` | Setting | Its type | |---|---| | the whole | `SiteConfig` | | `auth` | `SiteAuth` | | `defaultAccess` | `SiteAccess` : the values of `access` without code (`check` is for pages) | | `data` | `SiteData`, its tables `TableName` or `TableRule`, `allow` a list of `TableWrite` | | `cache` | `CacheConfig`, as a page has it | | `security` | `SiteSecurity` | | `seo` | `SiteSeo` | | `redirects` | a list of `SiteRedirect` | | `maintenance` | `true`, or a `SiteMaintenance` | | `limits` | `SiteLimits` | The server reads the file itself when it is saved, and tells what it can not read as a problem of the website : a file which TypeScript does not check (an editor of your own without the declarations) is checked all the same. ## Every setting | Setting | Default | What it is | |---|---|---| | `title` | the title of the website in the panel | The title of a page which has none, and `ctx.site.title`. | | `titleTemplate` | | Around the title of a page : `'%s · My shop'`. | | `description` | | The description of a page which has none. | | `lang` | `'en'` | The language of the pages. | | `domains` | `[]` | Hosts a reverse proxy serves the website at, at their root : for their requests, addresses are written without `/ssr/<apiPath>/<name>`. See [deployment](https://docs.apimaker.dev/v4/docs/ssr/deployment.html). | | `auth.providers` | `[]` | The auth providers of the account the visitors sign in with. See [authentication](https://docs.apimaker.dev/v4/docs/ssr/authentication.html). | | `auth.loginPath` | | Where a visitor who is not signed in goes for a page which asks for one (`?next=` added). Without it, such a visitor gets the page of the error `401`. | | `auth.sessionDays` | `7` | How long a session lasts, 1 to 365. | | `defaultAccess` | `'public'` | Who may open a page which says nothing : `'public'`, `'authenticated'`, `{ groups }`, `{ providers }`. | | `data.read` | `[]` | The tables `ctx.db` reads, each a name (`'instance/database/table'`) or a rule. See [data](https://docs.apimaker.dev/v4/docs/ssr/data.html#ctxdb-the-tables-a-website-lists). | | `data.write` | `[]` | The tables `ctx.db` writes, with `allow` to keep a table to some writes. | | `data.apiUser` | | The API user whose groups the calls of `ctx.api` carry, by its user name. | | `data.allowSys` | `false` | Turns `ctx.sys` on : everything the account may do. | | `data.fetch` | `[]` | The hosts `ctx.fetch` may ask : `'api.stripe.com'`, `'*.example.com'`. | | `cache` | none | How long the pages which say nothing are kept. See [caching](https://docs.apimaker.dev/v4/docs/ssr/caching.html). | | `security.csp` | `'strict'` | `'strict'`, `'relaxed'`, `'off'`, or a policy of your own (`{nonce}` is the nonce of the answer). See [security](https://docs.apimaker.dev/v4/docs/ssr/security.html#csp). | | `security.frameAncestors` | `["'self'"]` | Who may show the pages in a frame. | | `security.headers` | `{}` | Headers of every answer. The ones the server writes itself (cookies, length, type, location) are not taken. | | `seo.sitemap` | `true` | `/sitemap.xml`. | | `seo.robots` | `'allow'` | `/robots.txt` : `'allow'`, `'disallow'`, or its text. | | `seo.noIndex` | `false` | No page is indexed. | | `redirects` | `[]` | `{ from, to, status }` : `from` a path (`/old/*` for every path below it), `to` a path of the website or an address, `status` 301, 302, 307 or 308 (308). | | `trailingSlash` | `'never'` | `/about/` goes to `/about` (`'never'`), `/about` to `/about/` (`'always'`), or both answer (`'ignore'`). | | `maintenance` | `false` | `true`, or `{ message, allowGroups }` : the website answers `503` with the message, but to the groups which may see it. | | `limits.memoryMb` | `ssr.memoryMb` of the server | The memory of an engine of the website, 8 to 1024. | | `limits.cpuMs` | `ssr.cpuMs` | The time the code of one render may run, 10 to 60000. | | `limits.timeoutMs` | `ssr.timeoutMs` | The time of one render in all, the waits for data included, 100 to 600000. | ## Problems What can not be read is a problem of the file `site.config.ts`, which the panel shows (the button with the count of problems) with its words : `data.read[1] : 'shop/products' : a table is named 'instance/database/table'`. The website runs with the settings it read, and the defaults of the others — but a mistake never opens more : a `defaultAccess` which can not be read closes the pages which say nothing, an `access` of a page which can not be read closes that page, and rules of `data` which can not be read reach no table, until they are right. --- # Security - SSR websites > What the API Maker server does to keep SSR websites and their data safe by default - escaping, CSP with nonces, CSRF tokens, sealed sessions, tables and rows a website may reach, open redirects, isolation of websites, safe caching - and what is left to you. Source: https://docs.apimaker.dev/v4/docs/ssr/security.html A website runs code next to the data of the account. The server is built so that the code you write is safe by default, and so that a mistake opens as little as it can. ## What the server does | Risk | What the server does | |---|---| | **A script slipped into a page** (XSS) | Every value is escaped where it is written, in a text and in an attribute. `javascript:` and `vbscript:` addresses are refused when a file is saved, and when a value gives one. HTML which is not escaped is asked for by name : `raw()`, `dangerouslySetInnerHTML`. The text of a `<script>` or a `<style>` can not end its element. | | **Scripts of others** | A Content Security Policy on every page : the scripts of the website, of the UI libraries the page loads, and the ones with the nonce of the answer (a new one each time). Every `<script>` a page writes gets the nonce by itself ; attributes such as `onclick` do not run. | | **A post in the name of a visitor** (CSRF) | Every `<form method="post">` gets the CSRF token of its visitor ; a `POST`, `PUT`, `PATCH` or `DELETE` without a valid token is refused (`403`) before any code runs. The cookies of a website are `SameSite=Lax`. | | **A stolen or forged session** | A session is a cookie sealed with AES-256-GCM, under a key of the installation and of the website : it can not be read nor changed by the browser, and a session of one website is nothing to another. `HttpOnly`, `Secure` over HTTPS, for the path of the website. The cookies of API Maker are never seen by a page. | | **Reading what a website should not** | `ctx.db` reaches the tables `site.config.ts` lists, nothing else. A rule keeps the rows of each visitor (`$user`), checks a row read or changed by its id, and checks the tables `deep` and paths read on the way. Writes may be kept to `create`. See [data](https://docs.apimaker.dev/v4/docs/ssr/data.html). | | **More than the visitor may** | `ctx.api` calls the APIs as the visitor : their access, groups, tokens and hooks. `ctx.sys` is off unless the website turns it on. | | **A page opened by whom it should not** | `access` of a page and of its layouts, read when the file is saved : a visitor who may not is refused before any code of the page runs. A setting which can not be read closes the page, it never opens it. | | **A redirect to another site** (open redirect) | `redirect()` refuses another host unless the code says `{ external: true }` : a `?next=` a visitor wrote never sends to another site. | | **A request to an inner server** (SSRF) | `ctx.fetch` reaches the hosts `data.fetch` lists only. | | **A page shown in a frame of another site** (clickjacking) | `frame-ancestors 'self'` and `X-Frame-Options: SAMEORIGIN` unless the website lists who may frame it. | | **A kept page shown to someone else** | A page is kept for everyone only when it says so and the visitor is anonymous ; never one which read cookies, headers, the address or the token of its visitor, set cookies, or showed one-time messages. | | **One website against another** | Each website runs in engines of its own, with a limit of memory (`ssr.memoryMb`) and of time (`ssr.cpuMs`, `ssr.timeoutMs`). The objects of JavaScript are frozen : a page can not change them for the pages after it. Nothing of one website is seen by another. | | **Errors which say too much** | A visitor reads the message of an error thrown on purpose, and a sentence for any other : the stack and the lines of your files are shown in the preview of the panel only. | | **The browser guessing** | `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Cross-Origin-Opener-Policy: same-origin`, `Permissions-Policy` (no camera, microphone, geolocation, payment), `Strict-Transport-Security` over HTTPS. | ## The CSP | `security.csp` | The policy | |---|---| | `'strict'` (default) | `default-src 'self'` ; scripts of the website, of its libraries and with the nonce ; styles of the website, of its libraries and inline ; images from the website, `data:`, `blob:` and any HTTPS address ; fonts of the website and of its libraries ; requests (`fetch`) to the website ; forms post to the website ; no `<object>` ; frames of the same origin. | | `'relaxed'` | What HTTPS gives, inline scripts and `eval` included : for pages which load what they find. | | `'off'` | No policy. | | any other text | The policy itself, `{nonce}` written as the nonce of the answer. | A script of the page which needs the nonce in a value of its own (a library which adds scripts) finds it on its own `<script>` element : `document.currentScript.nonce`. ## The panel and the websites Serve the admin panel and the websites on **different origins** : another host, or another port. The panel of API Maker runs on a port of its own by default (`adminPanel.port`), so a website at `/ssr/<apiPath>/<name>` of the API port shares nothing with it. Behind a reverse proxy, give the panel and the websites different hosts : a website then can not read what the panel keeps in its browser. ## What is left to you - **Check every field a visitor sends**, on the server : what a browser checks a request skips. Take the fields you name, never `ctx.form` as it came. - **Never hand a value of a visitor to the database as a query** : `find: { category: String(ctx.query.category) }`, not `find: ctx.query`. Escape the text of a regular expression. - **Never give `raw()` what a visitor wrote.** - **Never write a value of `ctx.user` into a page kept for everyone** (`cache.scope: 'public'`). - **Keep `data.allowSys` off** unless a page needs everything the account may do ; then check every value which reaches it. - **List the tables a website needs, no more**, with the rows of each visitor (`where: { owner: '$user._id' }`), and `allow: ['create']` for a table visitors only add to. - **Serve the websites over HTTPS** (a reverse proxy with TLS) : the session cookie is `Secure` then, and browsers get `Strict-Transport-Security`. --- # Deployment - SSR websites > Serving an SSR website of API Maker at the root of its own domain - the domains setting, reverse proxies (nginx, Caddy, Traefik), TLS, forwarded headers, several servers, and the settings of the server for the websites. Source: https://docs.apimaker.dev/v4/docs/ssr/deployment.html A website answers at `/ssr/<apiPath>/<name>/` of the API Maker server, as soon as it is made : the path of its account (`apiPath`, as in the addresses of the APIs of the account), then its name — `https://api.example.com/ssr/acme/shop/products` for the website `shop` of the account `acme`. A name is unique in its account : two accounts may each have a `shop`. To show a website at the root of a domain of its own (`https://shop.example.com/products`), put a reverse proxy in front. ## A domain of its own 1. List the domain in `site.config.ts` : **site.config.ts** ```ts export default defineSite({ domains: ['shop.example.com', 'www.shop.example.com'] }); ``` For a request of such a host, the server writes the addresses of the website without `/ssr/acme/shop` : `href="/products"`, `asset('/app.css')`, the redirects, the cookies (`Path=/`), the canonical addresses. 2. Point the proxy at the website, keeping the host of the request. **nginx** ```nginx server { listen 443 ssl; server_name shop.example.com; ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem; location / { proxy_pass http://127.0.0.1:48246/ssr/acme/shop/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; client_max_body_size 20m; } } ``` **Caddy** ```caddy shop.example.com { rewrite * /ssr/acme/shop{uri} reverse_proxy 127.0.0.1:48246 { header_up Host {host} } } ``` **Traefik** ```yaml http: routers: shop: rule: Host(`shop.example.com`) service: api-maker middlewares: [shop-prefix] tls: { certResolver: letsencrypt } middlewares: shop-prefix: addPrefix: { prefix: /ssr/acme/shop } services: api-maker: loadBalancer: passHostHeader: true servers: [{ url: http://127.0.0.1:48246 }] ``` The settings view of a website in the panel writes these for its address. ## HTTPS Terminate TLS at the proxy and send `X-Forwarded-Proto: https` : the session cookie is `Secure` then (and `__Host-` at the root of a domain), and the answers carry `Strict-Transport-Security`. The origin a page knows (`ctx.site.origin`, the whole addresses of Open Graph and of feeds) is built from the host and that header. ## The panel and the websites Keep the admin panel on another host than the websites : `admin.example.com` for the panel, `shop.example.com` for the shop. A website then shares nothing with the panel in the browser. The panel runs on a port of its own by default (`adminPanel.port`). ## Several servers Every server of the installation answers every website : they read the websites from the database, and a save in the panel (or a pull of git) tells each to build the website again. Pages kept by the cache are kept by each server ; a save drops them everywhere. ## The settings of the server The websites share the server with the APIs. Their engines, threads and cache are set in the configuration of API Maker (`ssr.*`), and a website may ask for less or more for itself (`limits` of `site.config.ts`). | Setting | Default | What it is | |---|---|---| | `ssr.threads` | the cores of the machine | Threads which render pages, each with its engines | | `ssr.enginesPerThread` | 64 | Websites a thread keeps loaded ; the one used longest ago goes | | `ssr.memoryMb` | 64 | Memory of an engine : a render past it fails, the engine is made anew | | `ssr.cpuMs` | 2000 | Time the code of one render may run : a loop which does not end is stopped | | `ssr.timeoutMs` | 30000 | Time of one render in all, the waits for data included | | `ssr.cacheMb` | 64 | Size of the pages kept by the cache, 0 for none | See [API Maker configuration](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html#ssr). --- # Git - SSR websites > SSR websites in the git repository of an account - src/ssr/<website>/ with its files, website.yaml, and a tsconfig.json and the declarations of am-ssr for editors; commit, pull and revert from the Git page. Source: https://docs.apimaker.dev/v4/docs/ssr/git.html The websites of an account are part of its [git repository](https://docs.apimaker.dev/v4/docs/Git/git.html), a folder each : ```text src/ ssr/ shop/ website.yaml its name, its title, its description, its template, whether it answers tsconfig.json written for editors : JSX, @/ is the root of the website am-ssr.d.ts written for editors : the declarations of the module am-ssr and of JSX site.config.ts libraries.json pages/ _layout.tsx index.tsx products/[slug].tsx components/ProductCard.tsx lib/shop.ts data/products.json public/ styles.css logo.png a file which is no text is in the repository as its bytes blog/ … ``` ## Commit, pull, revert On the Git page, **SSR Websites** is one of the parts a commit takes, like custom APIs or schemas. - **Commit** writes every website of the account into `src/ssr/` and pushes what changed. - **Pull** makes every website of the folder what the branch has : a website it has not is made, its settings and its files are changed where they differ (each file is compiled), files the branch has not are taken away. A website of the account the branch has not is taken away. A website of the branch whose name belongs to another account of the installation is refused (the names of websites are unique in an installation) : rename its folder. - **Status** lists each changed file of a website, with its diff ; **revert** takes back a file, or a whole website, as the commit of the branch has it. ## The history of a file Every file of a website has its history : right click it in the tree, **Git history**, or above the editor. A folder has one (**Git history** in its menu : every file below it, a tab each), and so has the whole website (**Git history of the website** in the `⋮` menu of the files). The dialog lists every commit of the branch which changed it, newest first, under the version the website has now : - **Changes** shows the selected version against the one below it, against what the website has now, or against a version pinned in the list ; **Full version** shows it whole. - **Load into the editor** (a text) puts a version into the editor without saving it : read it, change it, then save it — or close it. - **Revert** writes the version into the website : a file as the commit has it, a folder with every file below it (a file the commit has not is removed), the whole website with its `website.yaml`. Nothing is committed : review it, then commit it on the Git page. - A file removed since keeps its history : revert it to a version which had it and it comes back. - A file which is no text — an image, a font — reads as what it is : its format, its pixels, its size and the start of its SHA-256. A [compressed image](https://docs.apimaker.dev/v4/docs/ssr/images.html) reads `2400 × 1600 pixels, 1.21 MB` in one version and `1920 × 1280 pixels, 236.4 KB` in the next. **Load older versions** reads further back : the history reads the last 50 commits of the branch first, 1 000 at most. ## Your editor `tsconfig.json` and `am-ssr.d.ts` make each folder of a website a project TypeScript checks as it is : open `src/ssr/shop` in VS Code or WebStorm and the pages are checked with the types of `am-ssr` (`PageProps`, `ctx`, the attributes of every element), `@/components/Card` is found, and a mistake is marked before you push. They are written for editors and never read back : the server writes them again at every commit. A `tsconfig.json` of the website's own (made in the panel) is the website's file, and is kept. The `tsconfig.json` at the root of the repository leaves `src/ssr` to these. ## The local client The [local client](https://docs.apimaker.dev/v4/docs/features/developer-tools.html#your-own-editor) of the account keeps the same folders on your disk, in sync both ways : - A file saved in your editor sends the folder of its website, saved as a pull saves it : each file is compiled, and files the folder has not are taken away. A new folder in `src/ssr/` is a new website ; a folder deleted takes its website away. - What the panel saves comes to your disk : a saved file with the rest of its website ; a file removed or moved, a website renamed or removed, with a full sync. - The name of a website is its folder. `website.yaml` holds its title, its description and whether it answers. - `tsconfig.json` and `am-ssr.d.ts` come from the server : what you change in them is not read. - A file whose path has a part which starts with `.` (`public/.well-known/security.txt`) is not synced by the local client, and stays on the server as it is. Add or change it in the panel, or with git. --- # Templates - SSR websites > The websites a new SSR website starts from - Starter, Blank, Blog, Shop, Dashboard, Landing page and Documentation - and the models a new page, layout, endpoint or component starts as. Source: https://docs.apimaker.dev/v4/docs/ssr/templates.html A new website starts from a template. Each one is a whole website you change as you like ; every page has comments which say why it is written so, for you and for an AI assistant. Each `README.md` lists its pages and what they show. | Template | What it is | UI libraries | Needs | |---|---|---|---| | **Starter** | One example page per feature : loaders, dynamic and catch-all routes, forms and actions, components, a grid of a table (search, sort, pages), a form which inserts rows, a sign-in and private pages, the cache, cookies, scripts in the browser, UI libraries, partials, endpoints. The place to learn. | Chart.js, on one page | Nothing ; the grid and the form use the table `contacts`, the sign-in the auth provider `customers` | | **Blank** | A layout, a home page and a page for 404. | | Nothing | | **Blog** | Posts with tags and authors, search, pages of results, an RSS feed, Open Graph, JSON-LD, a sitemap of every post. Posts from `data/posts.json`, or a table. | highlight.js | Nothing | | **Shop** | Products with filters and pages, a cart in a cookie, sign-in and sign-up, a checkout which saves orders, the orders of each customer which nobody else reaches. | Bootstrap, Bootstrap Icons | Runs on sample products ; orders need the tables and the auth provider `customers` | | **Dashboard** | A support desk behind a sign-in : numbers and charts, tickets with a search as you type (htmx), forms to create and change them, removing for the group `admin`, pages only that group opens. | Bootstrap, Bootstrap Icons, Chart.js, htmx | The auth provider `staff` ; the table `tickets` to save | | **Landing page** | A product page in Tailwind CSS : hero, features, prices, questions, a newsletter form whose table the website may only add to. | Tailwind CSS | Nothing to show it | | **Documentation** | Pages in sections from JSON, a catch-all route, a navigation, search, the page before and after, headings with anchors. | highlight.js | Nothing | The panel shows, for each, what it shows, its libraries, what it needs, and the pages worth opening first. ## New files **New file** in the tree of a website starts a file from a model : | Model | Kind | Where | |---|---|---| | Page | Page | `pages/new-page.tsx` | | Page with data | Page | `pages/items/index.tsx` : a loader with `ctx.db`, a search, pages | | Page of a record | Page | `pages/items/[id].tsx` : `getById`, `notFound()` | | Form | Page | `pages/contact.tsx` : an action, its checks, the message after it | | Page for signed-in visitors | Page | `pages/account.tsx` : `access` | | Cached page | Page | `pages/news.tsx` : `cache` with a tag | | Sign-in page | Page | `pages/login.tsx` : `ctx.auth.login` | | Layout | Layout | `pages/_layout.tsx` | | Middleware | Layout | `pages/_middleware.ts` | | Page for 404, Page for errors | Layout | `pages/_404.tsx`, `pages/_error.tsx` | | JSON endpoint | Endpoint | `pages/api/hello.ts` | | Endpoint of a table | Endpoint | `pages/api/items.ts` : GET a list, POST a record, with the rules of the website | | Sign-out endpoint | Endpoint | `pages/logout.ts` | | HTML partial | Endpoint | `pages/partials/item.ts` : HTML without the layout, for htmx | | RSS feed | Endpoint | `pages/feed.xml.ts` | | Component | Component | `components/<Name>.tsx` | | Code module | Code | `lib/<name>.ts` | Every template and every model is compiled and rendered by the tests of the server : what you start from works. --- # Examples - SSR websites > Recipes for SSR websites of API Maker - lists with search and pages, record pages, a customer's own data, admin-only sections, sign-in flows, uploads, JSON APIs, webhooks, htmx partials, charts, client scripts with CSRF, i18n, feeds, redirects, maintenance and more. Source: https://docs.apimaker.dev/v4/docs/ssr/examples.html Short recipes for what websites do most. Each is a whole file you can paste : the [templates](https://docs.apimaker.dev/v4/docs/ssr/templates.html) have many more, with comments. ## A list with a search and pages **pages/products/index.tsx** ```tsx import { paginate } from 'am-ssr'; import type { PageConfig, PageContext, PageProps } from 'am-ssr'; const TABLE = { instance: 'mongodb', database: 'shop', table: 'products' }; export const config: PageConfig = { title: 'Products', cache: { maxAge: 60, scope: 'public', tags: ['products'], query: ['q', 'page'] }, }; export async function loader(ctx: PageContext) { const q = String(ctx.query.q ?? '').trim().slice(0, 60); // A text of a visitor is a value of the query, never an operator. const find = q ? { name: { $regex: q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' } } : {}; const total = await ctx.db.count({ ...TABLE, find }); const pages = paginate({ page: String(ctx.query.page ?? '1'), perPage: 12, total }); const products = await ctx.db.query({ ...TABLE, find, sort: { name: 1 }, skip: pages.skip, limit: pages.limit }); return { q, pages, products }; } export default function Products({ data }: PageProps<typeof loader>) { const link = (page: number) => `/products?q=${encodeURIComponent(data.q)}&page=${page}`; return ( <> <form method="get"><input type="search" name="q" value={data.q} /> <button>Search</button></form> <ul>{data.products.map((product) => <li><a href={`/products/${product._id}`}>{product.name}</a></li>)}</ul> <nav> {data.pages.items.map((item) => item === '…' ? <span>…</span> : <a href={link(item)} class={item === data.pages.page ? 'current' : undefined}>{item}</a>, )} </nav> </> ); } ``` ## The page of a record **pages/products/[id].tsx** ```tsx import { HttpError, notFound } from 'am-ssr'; import type { PageConfig, PageContext, PageProps } from 'am-ssr'; export async function loader(ctx: PageContext) { try { const product = await ctx.db.getById({ instance: 'mongodb', database: 'shop', table: 'products', id: String(ctx.params.id) }); if (!product) notFound(); return { product }; } catch (error) { // An id which is no id, or no such record : the page of 404. if (error instanceof HttpError && error.status < 500) notFound(); throw error; } } export const config: PageConfig = { title: (data) => data.product.name, description: (data) => data.product.description }; export default function Product({ data }: PageProps<typeof loader>) { return <h1>{data.product.name}</h1>; } ``` ## The data of each customer, nobody else's **site.config.ts** ```ts data: { read: [{ table: 'mongodb/shop/orders', where: { customer_id: '$user._id' }, requireUser: true }], write: [{ table: 'mongodb/shop/orders', where: { customer_id: '$user._id' }, requireUser: true, allow: ['create'] }], }, ``` **pages/account/orders.tsx** ```tsx export const config: PageConfig = { title: 'Your orders', access: 'authenticated' }; export async function loader(ctx: PageContext) { // `find: {}` : the rule adds `customer_id: <the visitor>` to the query. return { orders: await ctx.db.query({ instance: 'mongodb', database: 'shop', table: 'orders', find: {}, sort: { createdAt: -1 } }) }; } ``` A saved order gets `customer_id` of the visitor whatever the page sent ; `getById` of another customer's order finds nothing. ## A part of the website for one group **pages/admin/_layout.tsx** ```tsx import type { LayoutProps, PageConfig } from 'am-ssr'; // Every page of admin/ : the group admin only. The others get 403 before any code of these pages runs. export const config: PageConfig = { access: { groups: ['admin'] } }; export default function AdminLayout({ children }: LayoutProps) { return <section class="admin">{children}</section>; } ``` ## The whole website behind a sign-in **site.config.ts** ```ts auth: { providers: ['staff'], loginPath: '/login' }, defaultAccess: 'authenticated', ``` **pages/login.tsx** ```ts export const config: PageConfig = { title: 'Sign in', access: 'public' }; ``` ## Who may open a page, decided by code ```ts export const config: PageConfig = { access: { authenticated: true, // Run before the loaders : `false` is a 403. check: async (ctx) => (await ctx.db.count({ instance: 'mongodb', database: 'app', table: 'members', find: { team: String(ctx.params.team) } })) > 0, }, }; ``` The rule of the table `members` (`where: { user_id: '$user._id' }`) keeps the count to the visitor's own memberships. ## A JSON API for the scripts of the website **pages/api/cart.ts** ```ts import { json } from 'am-ssr'; import type { PageContext } from 'am-ssr'; export async function GET(ctx: PageContext) { return { items: JSON.parse(decodeURIComponent(ctx.cookies.get('cart') ?? '%5B%5D')) }; } export async function POST(ctx: PageContext) { // The server checked the token in `x-csrf-token` before this runs. const slug = String(ctx.body?.slug ?? ''); if (!/^[a-z0-9-]{1,80}$/.test(slug)) return json({ message: 'No such product.' }, { status: 400 }); // ... return json({ added: slug }, { status: 201 }); } ``` **public/app.js** ```js const { base, csrf } = window.__AM_SSR__; await fetch(base + '/api/cart', { method: 'POST', headers: { 'content-type': 'application/json', 'x-csrf-token': csrf }, body: JSON.stringify({ slug: 'basalt-mug' }), }); ``` ## A webhook of another service **pages/hooks/payments.ts** ```ts import type { PageConfig, PageContext } from 'am-ssr'; // Other servers call it : no CSRF token, a secret of its own. export const config: PageConfig = { csrf: false, access: 'public' }; export async function POST(ctx: PageContext) { const secret = await ctx.cache.get<string>('payments-secret'); if (!secret || ctx.headers['x-signature'] !== secret) ctx.error(401, 'Unknown sender.'); await ctx.db.updateMany({ instance: 'mongodb', database: 'shop', table: 'orders', find: { paymentId: String(ctx.body?.id) }, updateData: { status: 'paid' } }); return { ok: true }; } ``` ## Uploading a picture **pages/account/avatar.tsx** ```tsx export const config: PageConfig = { access: 'authenticated' }; export async function action(ctx: PageContext) { const file = ctx.files.find((one) => one.fieldname === 'avatar'); if (!file || !/^image\/(png|jpeg|webp)$/.test(file.mimetype) || file.size > 2_000_000) { ctx.status(422); return { error: 'A PNG, JPEG or WebP image of 2 MB at most.' }; } ctx.flash('success', 'Saved.'); ctx.redirect('/account', 303); } export default function Avatar({ actionData }: PageProps<undefined, { error: string }>) { return ( <form method="post" enctype="multipart/form-data"> {actionData?.error && <p class="error">{actionData.error}</p>} <input type="file" name="avatar" accept="image/png,image/jpeg,image/webp" required /> <button type="submit">Upload</button> </form> ); } ``` ## A part of a page from the server, with htmx **libraries.json** ```json { "libraries": [{ "id": "htmx", "pages": ["/tickets"] }] } ``` **pages/tickets/index.tsx** ```tsx <input type="search" name="q" hx-get="/tickets/rows" hx-trigger="input changed delay:300ms" hx-target="#rows" /> <table><tbody id="rows"><TicketRows tickets={data.tickets} /></tbody></table> ``` **pages/tickets/rows.ts** ```ts import TicketRows from '@/components/TicketRows'; export async function GET(ctx: PageContext) { const tickets = await ctx.db.query({ instance: 'mongodb', database: 'desk', table: 'tickets', find: {}, limit: 20 }); return ctx.html(await TicketRows({ tickets })); } ``` `hx-get` gets the base path of the website by itself, as `href` does. ## A chart ```tsx export const config: PageConfig = { libraries: ['chartjs'] }; export default function Sales({ data }: PageProps<typeof loader>) { return ( <> <canvas id="sales"></canvas> <script>{`document.addEventListener('DOMContentLoaded', () => new Chart(document.getElementById('sales'), { type: 'bar', data: ${serialize({ labels: data.labels, datasets: [{ label: 'Sales', data: data.values }] })}, }));`}</script> </> ); } ``` ## Pages in the language of the visitor **lib/i18n.ts** ```tsx import texts from '@/data/texts.json'; export function t(ctx: PageContext, key: string): string { const lang = ctx.locale.startsWith('fr') ? 'fr' : 'en'; return (texts as Record<string, Record<string, string>>)[lang]?.[key] ?? key; } ``` A page which reads `ctx.locale` is never kept for everyone : use paths per language (`pages/fr/...`) for pages you keep. ## A feed **pages/feed.xml.ts** ```ts import { escape } from 'am-ssr'; export async function GET(ctx: PageContext) { const home = ctx.site.origin + ctx.site.base; const posts = await ctx.db.query({ instance: 'mongodb', database: 'blog', table: 'posts', sort: { date: -1 }, limit: 20 }); const items = posts.map((post) => `<item><title>${escape(post.title)}${home}/posts/${post.slug}`).join(''); ctx.setHeader('content-type', 'application/rss+xml; charset=utf-8'); return ctx.text(`${escape(ctx.site.title ?? '')}${items}`); } ``` ## Old addresses, and a pause **site.config.ts** ```ts redirects: [ { from: '/shop/*', to: '/products', status: 301 }, { from: '/twitter', to: 'https://x.com/apimaker', status: 302 }, ], // The website answers 503 with this message, but to the group admin. maintenance: { message: 'Back at 10:00 UTC.', allowGroups: ['admin'] }, ``` ## A redirect to another site ```ts // Refused without `external` : a `?next=` a visitor wrote never sends to another site. ctx.redirect('https://checkout.stripe.com/c/pay/' + session.id, 303, { external: true }); ``` ## Calling a custom API as the visitor ```ts export async function action(ctx: PageContext) { // The access of the API, its hooks, and the token of the visitor. const quote = await ctx.api.customApi('/shop/quote', { method: 'POST', body: { items: ctx.form.items } }); return { quote }; } ``` ## Values kept between requests ```ts const weather = await ctx.cache.getOrSet('weather:paris', 600, async () => { const answer = await ctx.fetch('https://api.example.com/weather?city=paris'); return answer.json; }); ``` --- # Reference - SSR websites > Every export of the module am-ssr, every member of ctx, PageConfig, the props of pages, layouts and error pages, and what a page, a layout, a middleware and an endpoint export. Source: https://docs.apimaker.dev/v4/docs/ssr/api-reference.html The declarations are the module `am-ssr` (the panel gives them to its editor ; a commit writes them as `am-ssr.d.ts` next to each website). ## What a file exports | File | Exports | |---|---| | A page (`pages/**/*.tsx`) | `default` (the component), `loader`, `action`, `config`, `head` | | A layout (`_layout.tsx`) | `default` (gets `children`), `loader`, `config`, `head` | | A middleware (`_middleware.ts`) | `default` (or `middleware`) : `(ctx) => answer | undefined` | | `_404.tsx`, `_error.tsx` | `default` (gets `error`), `config`, `head` | | An endpoint (`pages/**/*.ts`) | `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `ALL`, `config` | | `site.config.ts` | `default` : `defineSite({ ... })`, values only. See [settings](https://docs.apimaker.dev/v4/docs/ssr/site-config.html). | ## The module `am-ssr` | Export | What it is | |---|---| | `raw(html)` | HTML written as it is. Never what a visitor wrote. | | `url(path)` | An address of the website with its base path, for a value which is no attribute. | | `asset(path)` | A file of `public/` with the hash of its content : kept a year by browsers. | | `withoutBase(path)` | An address of the server outside the website. | | `serialize(value)` | JSON a ``. 6. **Forms** post to their own page : `
` gets its CSRF token by itself ; the page exports `action(ctx)` ; read `ctx.form` ; return `{ values, errors }` with `ctx.status(422)` on a mistake ; on success `ctx.flash('success', '…')` then `redirect('/path', 303)`. 7. **Scripts which POST** send `x-csrf-token: window.__AM_SSR__.csrf` and use `window.__AM_SSR__.base + '/api/...'`. 8. **Data** : `ctx.db.({ instance, database, table, ... })` reaches only the tables `site.config.ts` lists in `data.read` / `data.write` (`'instance/database/table'` or `{ table, where, set, requireUser, fields, allow }`). When you add a call to a new table, **add the table to `site.config.ts`**. For data of a visitor, add a rule `where: { owner_id: '$user._id' }` rather than filtering in the page. 9. **Never pass an object of the visitor as a query** : `find: { status: String(ctx.query.status) }`, never `find: ctx.query`. Escape regular expressions. 10. **Who may open a page** : `config.access`, or `access` of a `_layout.tsx` for a whole folder, or `defaultAccess` of the website. Do not check it only in the loader. 11. **Sign-in** : `await ctx.auth.login({ provider: '', username, password })` in an action, inside `try/catch` (`HttpError` with `status < 500` is a wrong password : answer `401` with one sentence for both cases). Sign-out is a `POST` endpoint calling `ctx.auth.logout()`. 12. **Redirects** to another host need `{ external: true }`. A `next` of the query is a path : check `next.startsWith('/') && !next.startsWith('//')`. 13. **Cache** : `cache: { maxAge, scope: 'public' }` only for pages which show nothing of the visitor (no `ctx.user`, no cookies). Drop with `tags` + `ctx.revalidate('tag')` after a change. 14. **Never keep `ctx` or a visitor's data in a variable of a module** : modules are shared by every visitor. 15. **No Node.js** : no `fs`, `process`, `Buffer`, `require('x')` of npm, `setTimeout`. Use `ctx.fetch` (hosts listed in `data.fetch`), `ctx.cache`, `crypto.randomUUID()`, `URL`, `TextEncoder`. 16. **Endpoints** return an object (JSON), a text, `json(data, { status })`, `ctx.html(await Component(props))` for HTML without the layout, or nothing (`204`). 17. **Browser libraries** (Bootstrap, Tailwind, Chart.js, htmx...) go in `libraries.json` (`{ "id": "bootstrap" }`) ; a page may limit them with `config.libraries: ['chartjs']`. Never add a `