# API Maker Documentation (full text), the code in Kotlin > 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 Kotlin only, and the links lead to the Markdown of the pages in Kotlin. The same with the code of another language : TypeScript https://docs.apimaker.dev/ai/typescript/llms-full.txt · Python https://docs.apimaker.dev/ai/python/llms-full.txt · PHP https://docs.apimaker.dev/ai/php/llms-full.txt · C# https://docs.apimaker.dev/ai/csharp/llms-full.txt · Go https://docs.apimaker.dev/ai/go/llms-full.txt · Rust https://docs.apimaker.dev/ai/rust/llms-full.txt · Java https://docs.apimaker.dev/ai/java/llms-full.txt. --- # API Maker Documentation > Learn API Maker step by step, in simple words and with real screens - what it is, install it with API Maker Launchpad, make your first API, add your own code, secure it, and put it on your own server. Source: https://docs.apimaker.dev/index.html API Maker v4.3.0 · The backend engine for the AI era **Connect a database. Get its APIs. Add your logic.** API Maker connects to your database and gives every table ready-made, secure REST APIs. When you need more, you write your own logic in the language you like. You do it all in one admin panel, in your browser, and it runs on your own computer or servers. - [Start learning](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.md) - [Install API Maker](https://docs.apimaker.dev/v4/docs/getting-started/local-run.md) - [Your first API](https://docs.apimaker.dev/v4/docs/getting-started/first-api.md) ## What is API Maker? > Diagram : API Maker in one picture : your apps → API Maker (ready-made APIs, your code, security) → your databases > > API Maker in one picture : your apps call API Maker ; API Maker gives every table of your databases ready-made APIs, runs your own code and checks who may call what, then reads and writes your databases. You set it up in the admin panel, in your browser. API Maker Launchpad, a desktop app, installs it on your computer or on your servers. - **You connect a database.** MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB or Percona. - **Every table gets its APIs at once.** Read, search, save, update and delete. You write no code for them. - **You add your own logic when you need it.** A custom API is one function, in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin. - **Nothing is open until you say so.** You choose which app may call which API. - **You work in one admin panel**, in your browser. **API Maker Launchpad**, a free desktop app, installs everything for you. ## Learn API Maker in 7 steps Follow the steps in order. Each one is short, and each page shows the real screens. 1. [How API Maker works](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.md) : The big picture, in simple words : apps, APIs, your code, your databases. · 5 min read 2. [Install API Maker](https://docs.apimaker.dev/v4/docs/getting-started/local-run.md) : Launchpad installs and starts it on your computer, in a few clicks. · 10 min 3. [The admin panel](https://docs.apimaker.dev/v4/docs/getting-started/admin-panel.md) : Sign in, and learn where everything lives. · 5 min 4. [Your first API](https://docs.apimaker.dev/v4/docs/getting-started/first-api.md) : Connect a database, save a row, read it back, see a mistake refused. · 10 min 5. [Your own code](https://docs.apimaker.dev/v4/docs/getting-started/first-custom-api.md) : Write a custom API : one function behind a URL. Then call it. · 10 min 6. [Secure your APIs](https://docs.apimaker.dev/v4/docs/getting-started/secure-your-api.md) : Give each app a login and a list of what it may call. · 10 min 7. [Put it on a server](https://docs.apimaker.dev/v4/docs/getting-started/install-on-server.md) : Launchpad installs API Maker on your own server, with HTTPS. · 15 min New words on the way ? [Words to know](https://docs.apimaker.dev/v4/docs/getting-started/glossary.md) explains each one in a line. ## Find your way - [All the APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) : Every API of a table, custom APIs and system APIs, with their URLs. - [Your tables](https://docs.apimaker.dev/v4/docs/schema/schema.md) : The schema of a table : its fields, their rules, and the links between tables. - [Your code](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md) : Custom APIs, hooks, events and schedulers, in eight languages. - [Code examples](https://docs.apimaker.dev/v4/examples/index.kotlin.md) : A working example for every call your code can make. - [Security](https://docs.apimaker.dev/v4/docs/security/overview.md) : Who may call what, sign-in of your users, secrets, and the security report. - [Run it](https://docs.apimaker.dev/v4/docs/guides/index.md) : Caching, logs, Git, backups and upgrades. - [Websites](https://docs.apimaker.dev/v4/docs/ssr/index.md) : Websites rendered by the server, next to your data. - [Cheat sheet](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md) : URLs, headers and the most used calls, on one page. - [Docs for AI assistants](https://docs.apimaker.dev/v4/docs/ai/llms.md) : Point your AI assistant at these docs. - [Release notes](https://docs.apimaker.dev/v4/docs/blog/release-notes/release_notes_summary.md) : What changed in each version, up to v4.3.0. - [Benchmarks](https://docs.apimaker.dev/v4/docs/benchmarks/index.md) : How fast it is, measured on real servers. ## Watch What API Maker does, in under two minutes. Then the whole product in one demo, and your first API. - Video: [What Is API Maker? The Backend Engine for the AI Era](https://apimaker.dev/videos/what-is-api-maker) (2 min) - Video: [API Maker Demo: From Install to APIs Across Six Databases](https://apimaker.dev/videos/full-demo) (15 min) - Video: [How API Maker Works: The Life of a Request, from Your App to Your Data](https://apimaker.dev/videos/how-api-maker-works) (7 min) - Video: [Your First API in 10 Minutes: The API Maker Quick Start](https://apimaker.dev/videos/first-api-quick-start) (7 min) Every feature has a video, with chapters and a transcript : see the [video library](https://apimaker.dev/videos), from the [first steps](https://apimaker.dev/videos/playlists/start-here) to [the complete course](https://apimaker.dev/videos/playlists/complete-course). ## Stay in touch [Twitter](https://twitter.com/api_maker) · [LinkedIn](https://www.linkedin.com/company/api-maker) · [YouTube](https://www.youtube.com/@api_maker) · report a problem at [API Maker Planning](https://github.com/APIMaker-dev/API-Maker-Planning/issues) · write to [contact@apimaker.dev](mailto:contact@apimaker.dev). --- # How API Maker Works > The big picture of API Maker in simple words - what happens when an app calls an API, the kinds of APIs, who may call what, where your own code runs, and what runs on a server. Source: https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.html API Maker is a program that runs on a server. Your apps call it over the internet. It talks to your databases and runs your own code. This page shows each part once, in simple words, so the rest of the docs make sense. > **In short** > > - Your app calls a **URL**. API Maker checks **who** calls, runs the API, and answers in JSON. > - Every table gets **ready-made APIs**. Your own logic goes in **custom APIs** and **hooks**. > - Each app has a login : an **API user**. Its **groups** list what it may call. > - Your code runs in **sandboxes** : small containers, apart from API Maker. - Video: [How API Maker Works: The Life of a Request, from Your App to Your Data](https://apimaker.dev/videos/how-api-maker-works) (7 min) ## API Maker in one picture > Diagram : API Maker in one picture : your apps → API Maker (ready-made APIs, your code, security) → your databases > > API Maker in one picture : your apps call API Maker ; API Maker gives every table of your databases ready-made APIs, runs your own code and checks who may call what, then reads and writes your databases. You set it up in the admin panel, in your browser. API Maker Launchpad, a desktop app, installs it on your computer or on your servers. You do three things, in this order : 1. **Install it.** [API Maker Launchpad](https://docs.apimaker.dev/v4/docs/getting-started/local-run.md) does it in a few clicks. 2. **Build in the admin panel.** Connect a database, and its tables have APIs. Add your code where you need it. 3. **Call it from your apps.** Each app gets a login and a list of what it may call. ## What happens when your app calls an API > 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. 1. **Your app calls a URL**, for example `GET /api/schema/admin/mongodb/shop/products?limit=10`. It sends its token in the header `x-am-authorization`. 2. **API Maker checks the token.** No valid token : `401`. A token whose groups do not allow this API : `403`. 3. **Your pre hooks run**, if you wrote some. A hook can change the request : for example, keep a person to their own rows. 4. **The API runs.** It reads or writes the database, or runs your function. With caching on, a read can come from the cache. 5. **Your post hooks run**, if you wrote some. They can change the answer. 6. **API Maker answers** : `{ "success": true, "statusCode": 200, "data": … }`. When something is wrong, `errors` takes the place of `data`. More : [the answer](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md) · [the headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) · [error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md). ## The kinds of APIs > 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. | Kind | Its URL starts with | What it is | |---|---|---| | Schema APIs | `/api/schema/` | The ready-made APIs of a table. They check every write against the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) of the table : types, rules, default values. | | Generated APIs | `/api/gen/` | The same APIs, without the checks of a schema. | | Custom APIs | `/api/custom-api/` | [Your own function](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md), behind a path you choose. | | System APIs | `/api/system-api/` | Ready-made tools : [tokens](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.kotlin.md), encryption, the cache, indexes. | Your code can also run without a URL : [hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) (before or after an API), [events](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.kotlin.md) (after something happened), [schedulers](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md) (on a timer) and [WebSocket events](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.kotlin.md) (live messages to your apps). In a URL, `admin` is the **user path** of the account which owns the APIs. The first account of a new installation has `admin`. Each [developer account](https://docs.apimaker.dev/v4/docs/features/team-work.md#developer-accounts) has a path of its own. [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) lists every API with its URL. ## Who may call what > Diagram : Two tokens, two gates : the API user (which application) and the person (which rows) > > Two identities on every request : the API user token from the token API in x-am-authorization tells which application calls, and the token of a person from a token generator in x-am-user-authorization tells who the person is. Groups of the API user decide which APIs, tables and fields may be called ; pre hooks decide which rows belong to the person. There are two gates. - **The API gate : which app.** Each app gets an **API user** : a username and a password. The app trades them for a token at the [token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.kotlin.md), then sends the token on every call. The **groups** of the API user list the APIs, tables and fields it may use. See [API users](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.md) and [groups](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md). - **The row gate : which person.** When people sign in to your app, a second token says who the person is, in `x-am-user-authorization`. A pre hook then keeps each person to their own rows. See [sign-in of your users](https://docs.apimaker.dev/v4/docs/authorization/AMDB.md). A new account starts with an API user `default` and a group `Default`, which allows everything. The password of `default` is kept in the default secret, at `common.apiUserPasswords.default`. It is empty on a new account, unless your installation sets one : type one there before you ask for a token. Before you go live, replace both : one small group per screen of your app, as [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.kotlin.md) shows. Some APIs need no token at all : set their access to public (`IS_PUBLIC`) in their [settings](https://docs.apimaker.dev/v4/docs/settings/apiSettings.md). ## Where your code runs > Diagram : Where code runs : sandboxes per account and language, and the native process > > One API Maker process per server hands the code of each account to the sandboxes of that account : Docker containers, one kind per language, more of them while they are busy, up to the sandbox count. Code marked runOnNativeProcess runs on the machine instead, beside API Maker. The containers run without privileges but without a memory limit : they guard against faulty code more than against hostile code. - **In a sandbox, by default.** A sandbox is a Docker container, apart from API Maker. Each language has its own sandboxes, and API Maker starts more of them when many calls come. A call has a time limit : 13 seconds unless you set another. See [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md). - **On the machine, when you ask.** Code marked **Run on native process** runs next to API Maker, without Docker. It is faster and less isolated : use it for a browser with [Playwright](https://docs.apimaker.dev/v4/docs/guides/browser-automation.md), or the lowest delay. See [native process](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md#native-process). - **In eight languages.** TypeScript (on Node.js or Bun), Python, PHP, C#, Go, Rust, Java and Kotlin. See [Languages](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md). - **With one object, `g`.** Your code gets the request (`g.req`), the answer (`g.res`) and every API of API Maker (`g.sys.db`, `g.sys.system`, `g.sys.cache`). See [the global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md). You write the code in the admin panel, with completion of your tables and fields. Or in your own editor : the [local client](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#your-own-editor) keeps a folder in sync, both ways. ## What runs on a server > Diagram : One server set up by API Maker Launchpad : the reverse proxy → systemd (API Maker : APIs, WebSockets, admin panel) → Docker (MongoDB, Redis, sandboxes) → your databases > > What API Maker Launchpad sets up on one Ubuntu server : a reverse proxy (Caddy, Ferron or Pingap) on ports 80 and 443 in front, API Maker run by systemd as the service api_maker_be (one program : the APIs and the WebSockets on port 38246, the admin panel on port 4626), MongoDB and Redis in Docker for the internal data of API Maker, sandbox containers for your code, and your own databases anywhere. - **API Maker** : one program, kept running by systemd. It uses every core of the server. - **A reverse proxy** (Caddy, Ferron or Pingap) in front of it, for HTTPS. - **MongoDB and Redis**, in Docker. MongoDB keeps the settings and the code of API Maker. Redis keeps the cache. Your own data stays in your databases. - **The sandboxes** of your code. - **The admin panel**, served by API Maker itself. You do not set this up by hand : [Launchpad does it](https://docs.apimaker.dev/v4/docs/getting-started/install-on-server.md). For more servers, see the [architectures](https://docs.apimaker.dev/v4/docs/getting-started/install-on-server.md#architectures). ## What happens when you save a row > 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. A schema API checks a row before it reaches the database. It trims spaces, fixes types and fills default values, then runs the rules of the schema and your own checks. When something is wrong, you get `400` with every problem at once, and nothing is saved. See [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md). ## Everything is in Git Your schemas, APIs, code and settings are files. The admin panel commits them to a Git repository, and another server pulls them : that is how a change ships. Secrets never go to Git. See [Git integration](https://docs.apimaker.dev/v4/docs/Git/git.md). - Video: [What Is API Maker? The Backend Engine for the AI Era](https://apimaker.dev/videos/what-is-api-maker) (2 min) - Video: [API Maker v4: Months of Backend Work, Done in Minutes](https://apimaker.dev/videos/trailer) (1 min) - Video: [API Maker Admin Panel Tour: Find Any Page in Two Keystrokes](https://apimaker.dev/videos/admin-panel-tour) (4 min) --- # Install API Maker > Install API Maker on your computer with API Maker Launchpad, a free desktop app for macOS, Windows and Linux - download it, make a project, press Install & start, open the admin panel. Then add a database, and keep your code in your own editor. Source: https://docs.apimaker.dev/v4/docs/getting-started/local-run.html **API Maker Launchpad** installs API Maker for you. It is a free desktop app for macOS, Windows and Linux. You pick a folder and press a button : Launchpad downloads API Maker, starts the databases it needs, and opens the admin panel. > **In short** > > 1. Start **Docker**, then install **Launchpad**. > 2. In Launchpad, press **New project**, then **Install & start**. > 3. Press **Open admin panel**, and sign in with the email and password Launchpad shows. > Diagram : API Maker Launchpad : one folder, Docker for the databases, API Maker and its admin panel on your computer > > API Maker Launchpad on your computer : the desktop app picks a folder, downloads API Maker from npm (one native program, no Node.js), starts MongoDB and Redis and your extra databases in Docker, runs API Maker with its admin panel, and keeps a folder in sync with your editor through the local client. - Video: [Run API Maker on Your Computer with Launchpad, in One Click](https://apimaker.dev/videos/launchpad-local-mode) (10 min) ## What you need | | | |---|---| | Docker | Docker Desktop, OrbStack, Rancher Desktop, Colima or Docker Engine. Start it before Launchpad. | | Your computer | macOS 13 or later · Windows 10 or 11 · Linux : Ubuntu 22.04+, Debian 12+, Fedora 39+ | | Memory | about 4 GB free | ## 1. Download Launchpad **macOS** 13 Ventura or later · Apple silicon and Intel Universal · [Disk image **.dmg**](https://images.apimaker.dev/prod/api_maker_launchpad/API-Maker-Launchpad-1.0.0-macos-universal.dmg) · [Zip **.zip**](https://images.apimaker.dev/prod/api_maker_launchpad/API-Maker-Launchpad-1.0.0-macos-universal.zip) Open the .dmg and drag the app to Applications. **Windows** Windows 10 and 11 x64 · [Installer **.exe**](https://images.apimaker.dev/prod/api_maker_launchpad/API-Maker-Launchpad-1.0.0-windows-amd64-setup.exe) · [Portable **.zip**](https://images.apimaker.dev/prod/api_maker_launchpad/API-Maker-Launchpad-1.0.0-windows-amd64.zip) ARM64 · [Installer **.exe**](https://images.apimaker.dev/prod/api_maker_launchpad/API-Maker-Launchpad-1.0.0-windows-arm64-setup.exe) · [Portable **.zip**](https://images.apimaker.dev/prod/api_maker_launchpad/API-Maker-Launchpad-1.0.0-windows-arm64.zip) Installs for your user, no administrator rights needed. **Linux** Ubuntu 22.04+, Debian 12+, Fedora 39+ x64 · [Debian, Ubuntu **.deb**](https://images.apimaker.dev/prod/api_maker_launchpad/api-maker-launchpad_1.0.0_amd64.deb) · [Any distro **.tar.gz**](https://images.apimaker.dev/prod/api_maker_launchpad/API-Maker-Launchpad-1.0.0-linux-amd64.tar.gz) ARM64 · [Debian, Ubuntu **.deb**](https://images.apimaker.dev/prod/api_maker_launchpad/api-maker-launchpad_1.0.0_arm64.deb) · [Any distro **.tar.gz**](https://images.apimaker.dev/prod/api_maker_launchpad/API-Maker-Launchpad-1.0.0-linux-arm64.tar.gz) Needs GTK 3 and WebKitGTK 4.1 ; the .deb installs them. Version 1.0.0 · the SHA-256 checksum of every file and the install steps are on [apimaker.dev/download](https://apimaker.dev/download) On a Mac, open the `.dmg` and drag the app to **Applications**. If macOS blocks the first start, open **System Settings → Privacy & Security** and press **Open Anyway**. ## 2. Make a project Open Launchpad. On the welcome screen, check that **System check** shows Docker with a green tick, then press **New project**. [![The welcome screen of API Maker Launchpad, with the New project button](https://docs.apimaker.dev/assets/images/screens/lp-welcome.webp)](https://docs.apimaker.dev/assets/images/screens/lp-welcome.webp) The welcome screen of Launchpad. 1. **New project** makes a folder and sets API Maker up in it. 2. **System check** : Docker must show a green tick. Give the folder a name and choose where it goes, then press **Create**. Everything of the project stays in this folder : API Maker, its settings, its logs and the data of its databases. [![The New API Maker project dialog : a folder name, where to create it, and Create](https://docs.apimaker.dev/assets/images/screens/lp-new-project.webp)](https://docs.apimaker.dev/assets/images/screens/lp-new-project.webp) 1. A name for the folder of the project. 2. Where to create it. 3. **Create**. ## 3. Choose how it runs [![Set up API Maker in this folder : the choice between Native and Full Docker](https://docs.apimaker.dev/assets/images/screens/lp-architecture.webp)](https://docs.apimaker.dev/assets/images/screens/lp-architecture.webp) 1. **Native** : API Maker runs as a program, its databases in Docker. Pick this one. 2. **Full Docker** : everything runs in Docker. Keep **Native**, the default : API Maker runs as one program on your computer, and its databases run in Docker. It starts fastest and uses the least memory. **Full Docker** puts everything in Docker, if you prefer nothing outside Docker. You can switch later. Press **Next**. ## 4. Pick the version [![The project name and the version of API Maker to install](https://docs.apimaker.dev/assets/images/screens/lp-version.webp)](https://docs.apimaker.dev/assets/images/screens/lp-version.webp) 1. The name of your project. 2. The version of API Maker. The newest is chosen for you. 3. **Next**. Name the project. The newest version of API Maker is chosen for you, and so are the versions of MongoDB and Redis. Press **Next**. The next screen shows the ports. The defaults are free ports : keep them, and press **Next**. ## 5. Install and start [![Ready for take-off : the summary of the project and the Install and start button](https://docs.apimaker.dev/assets/images/screens/lp-review.webp)](https://docs.apimaker.dev/assets/images/screens/lp-review.webp) 1. What Launchpad will set up. 2. **Install & start**. Check the summary, then press **Install & start**. The first start downloads about 230 MB on a Mac (120 MB on Linux), then starts MongoDB, Redis and API Maker. The progress shows each step. It takes a minute or two. ## 6. Open the admin panel [![The dashboard of a running project : Open admin panel, the endpoints and the users to sign in with](https://docs.apimaker.dev/assets/images/screens/lp-dashboard.webp)](https://docs.apimaker.dev/assets/images/screens/lp-dashboard.webp) 1. **Open admin panel** opens it in your browser. 2. The addresses of the admin panel and of your APIs. 3. The email and password to sign in with. The eye shows the password. Press **Open admin panel**. Sign in with the **Admin** user of the card on the right : `admin@admin.com` and its password (the eye shows it). The **Root** user manages the installation itself : you rarely need it. That's it : API Maker runs on your computer. The next step shows [the admin panel](https://docs.apimaker.dev/v4/docs/getting-started/admin-panel.md). ## Add a database API Maker gives APIs to the tables of your databases. Launchpad can run a database for you, in Docker. Open the **Databases** page of the project and pick an engine : MongoDB, PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, TiDB or Percona. **Sample data** fills the new database with rows to try right away : the sample shop of these docs, or a well-known sample of that engine, such as Sakila, Pagila, Northwind, AdventureWorks LT or the sample data of MongoDB. Keep **None** for an empty database. [![The dialog to add a MySQL database in Launchpad : name, version, database and the Add button](https://docs.apimaker.dev/assets/images/screens/lp-databases.webp)](https://docs.apimaker.dev/assets/images/screens/lp-databases.webp) 1. A name for the database server. 2. Its version. 3. The database to make in it. 4. **Add MySQL**. Keep **Add to API Maker** on. Once the database runs, it is an **instance** of API Maker : a database server that API Maker reads and writes, its tables with their APIs. [![The card of a database in Launchpad, with Create instance in API Maker](https://docs.apimaker.dev/assets/images/screens/lp-create-instance.webp)](https://docs.apimaker.dev/assets/images/screens/lp-create-instance.webp) 1. **Create instance in API Maker** : its APIs are ready at once. A database you already have works too : [Your first API](https://docs.apimaker.dev/v4/docs/getting-started/first-api.md#1-connect-a-database) shows how to connect it. - Video: [Databases on Your Computer: 8 Engines, Any Version, One Click](https://apimaker.dev/videos/launchpad-databases) (8 min) ## Your code in your editor The **Local Client** page keeps a folder of your computer in sync with API Maker, both ways. Edit your custom APIs, schemas, hooks and test cases in VS Code, WebStorm, Zed or any editor. What you save in the admin panel lands in the folder too. [![The Local Client page of Launchpad : the folder kept in sync, and the buttons to open it in an editor](https://docs.apimaker.dev/assets/images/screens/lp-local-client.webp)](https://docs.apimaker.dev/assets/images/screens/lp-local-client.webp) 1. What was synced, both ways. 2. **Open in** your editor. - **Where the code is kept** : a Git repository on this computer (recommended), GitHub or GitLab, or a plain folder. - **Nothing is committed for you.** You commit when you want : from the page, from the Git page of the admin panel, or with git. - `npm run test` in the folder runs your [test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md). - More : [Developer tools](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#your-own-editor). - Video: [Code Your Backend in VS Code: Two-Way Sync with the Local Client](https://apimaker.dev/videos/vs-code-local-client) (5 min) ## Languages for native APIs Your code runs in sandboxes, which bring their own language. Code marked **Run on native process** runs on your computer instead, with a language installed there ([native process](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md#native-process)). A new project uses no native language until you choose some on its **Languages** page : - **Use them** takes the languages your computer has already : Launchpad looks on the PATH, in Homebrew, nvm, pyenv, rustup, SDKMAN and the like. - **Or pick a version**, and Launchpad downloads it from its publisher, checks it, and keeps it in `~/.api-maker-launchpad/languages`. Nothing is installed in your system. - Only API Maker sees them : nothing changes in your shell. | Language | Program | Note | |---|---|---| | Node.js, Bun | `node`, `bun` | | | Python | `python3` | 3.12 or later | | PHP | `php` | with FFI | | Go, Rust | `go`, `cargo` | need a C compiler (Xcode Command Line Tools, `gcc`) ; Go does not run native code on Windows | | C# | `dotnet` (the SDK) | | | Java, Kotlin | `java` (a JDK), `kotlinc` | Kotlin runs on the JDK | - Video: [Node.js, Python, Go and Java for Native APIs, Installed for You](https://apimaker.dev/videos/native-languages) (8 min) ## Several projects Each project has its own folder, its own ports and its own tab. Run several at once. **Delete** removes a project with everything it made, after you type its name. - Video: [Many Projects on One Computer: Tabs, Settings and Your Editor in Sync](https://apimaker.dev/videos/launchpad-projects) (7 min) ## Good to know > **The ports** > > New projects get free ports. These are the defaults : > > | Service | Port | > |---|---| > | API Maker : its APIs and its WebSockets | 38246 | > | Admin panel | 4626 | > | MongoDB of API Maker | 38248 | > | Redis of API Maker | 7479 | > | Your databases | MongoDB 27018 · PostgreSQL 5433 · MySQL 3307 · MariaDB 3308 · Percona 3309 · TiDB 4001 · SQL Server 14330 · Oracle 1522 | > > The services listen on `127.0.0.1` on macOS and Windows, and on `0.0.0.0` on Linux. The address is a project setting. > **The folder of a project** > > Launchpad keeps everything in the folder you picked : API Maker, its settings (`config/.env`, `config/docker-compose.yml`), the logs and the data of every database (`docker-data/…`). Docker only runs the containers : no data lives inside them. > **The license key** > > On your computer, Launchpad sets the key `____your_license_key____` : it is free, for development, and never sent to the license server (the license page of the admin panel shows `LOCAL`). A server that serves other people needs a key of its own : write to [contact@apimaker.dev](mailto:contact@apimaker.dev). [What the license check sends](https://docs.apimaker.dev/v4/docs/security/overview.md#licence-check). > **Closing the app** > > Closing Launchpad stops everything it started. If it closes by accident, a small watchdog stops API Maker and its containers for it. ## Cloud mode : your servers The **Cloud** switch, at the top right of Launchpad, turns it to your servers : it installs API Maker on servers you rent anywhere, over SSH, with HTTPS. The last step of the learning path shows it : [Put it on a server](https://docs.apimaker.dev/v4/docs/getting-started/install-on-server.md). --- # The Admin Panel > Find your way in the admin panel of API Maker - sign in, the four groups of pages of the home page, the menus, the search, the command palette, the guided tour and the sample shop of a new account. Source: https://docs.apimaker.dev/v4/docs/getting-started/admin-panel.html The admin panel is where you build everything : you connect databases, design tables, write code, test APIs and watch what happens. It runs in your browser, and API Maker serves it. > **In short** > > - Sign in with your email, your password and the letters of the picture. > - The home page has four groups of pages : **APIs & Logic**, **Security**, **Database** and **Operations**. > - Press **⌘K** (**Ctrl K** on Windows and Linux) to jump to any page. - Video: [API Maker Admin Panel Tour: Find Any Page in Two Keystrokes](https://apimaker.dev/videos/admin-panel-tour) (4 min) ## Sign in Open the address of the admin panel. On your computer, Launchpad shows it on the dashboard of the project : `http://127.0.0.1:4626` by default. [![The sign-in card of the admin panel : email, password, captcha and Login](https://docs.apimaker.dev/assets/images/screens/panel-login.webp)](https://docs.apimaker.dev/assets/images/screens/panel-login.webp) 1. Your email. 2. Your password. 3. Type the letters of the picture. 4. **Login**. Type your email and your password, then the letters of the picture : that is the captcha. Press **Login**. Launchpad shows the email and password of the **Admin** user. On a server, change the passwords before anyone else can reach it : see [Before you go live](https://docs.apimaker.dev/v4/docs/security/go-live.md). ## The home page [![The home page of the admin panel : four groups of pages and a search box](https://docs.apimaker.dev/assets/images/screens/panel-dashboard.webp)](https://docs.apimaker.dev/assets/images/screens/panel-dashboard.webp) 1. **Search** : type a word, the pages that match stay lit. 2. **APIs & Logic** : your APIs and your code. 3. **Security** : who may call what, secrets, the security report. 4. **Database** : tables, indexes, migrations. 5. **Operations** : logs, analytics, Git, backups, servers. | Group | What you do there | |---|---| | **APIs & Logic** | Connect your databases (**Instance API**). Write custom APIs, events, schedulers and WebSocket events. Try any API in **API Testing**. | | **Security** | Who may call what (**API User Permissions**, **API Group Permissions**), the sign-in of your users, your secrets, and the security report. | | **Database** | The diagram of your tables, their indexes, and migration scripts. | | **Operations** | Analytics, logs, servers, Git, backups and upgrades of API Maker. | ## Menus and search [![The APIs and Logic menu of the top bar of the admin panel](https://docs.apimaker.dev/assets/images/screens/panel-menu.webp)](https://docs.apimaker.dev/assets/images/screens/panel-menu.webp) 1. The menus of the top bar reach every page from anywhere. 2. The pages of that group. - **The menus** of the top bar reach every page, from any page. - **The search box** of the home page lights up the pages that match what you type. - **⌘K** (**Ctrl K**) opens the command palette : type a few letters of a page, then press Enter. - **?** on any page numbers its controls and says what each one does. ## The sample shop A new account is not empty. It holds a **sample shop** to read and to try : an instance named `mongodb`, the tables of a shop (categories, customers, products, orders), custom APIs, hooks and an event. [Your first API](https://docs.apimaker.dev/v4/docs/getting-started/first-api.md) uses it. ## The guided tour The panel has a guided tour. Press the **route icon** in the top bar to open it. It gives one small task at a time, on the real pages, and rings the control to press. A step ticks itself once it is done. Its first steps take a new account to a working API : the same path as these pages. - **Clean start**, its first step, removes the sample shop. Keep the sample if you follow these docs. - **Tour data** : see below. - The tour can be switched off in its own window. The icon stays, so you can come back. ### Tour data Right after your database answers, the tour offers **tour data** : a small shop of customers, products and orders, made in a new database the way your kind of database works. Every next step then has rows to show. The same step removes it in one click. It never touches your own tables. --- # Your First API in 10 Minutes > A hands-on start with the sample shop every new API Maker account has - connect MongoDB through the default secret, test the instance, save a category and a product in API Testing, read them back, see a refusal, then call the same API from your app with curl. Source: https://docs.apimaker.dev/v4/docs/getting-started/first-api.html A new account comes with a **sample shop** : an instance named `mongodb`, with the tables `categories`, `customers`, `products`, `orders` and `orderItems`. In ten minutes you point it at a database, save a product, read it back, and watch API Maker refuse a bad one. You do it all in the admin panel. At the end, your own app makes the same call. > **In short** > > 1. In the default **secret**, type a password for the API user `default` and the address of your MongoDB. > 2. In **Instance API**, press **Test Instance**. > 3. In **API Testing**, pick a table and an API, then press **SEND**. > **Tip : Nothing installed yet ?** > > [Install API Maker](https://docs.apimaker.dev/v4/docs/getting-started/local-run.md) first. Then add a MongoDB on the **Databases** page of Launchpad : its card shows the connection string to copy. - Video: [Your First API in 10 Minutes: The API Maker Quick Start](https://apimaker.dev/videos/first-api-quick-start) (7 min) ## 1. Connect a database An instance never holds the password of its database. The password stays in the **default secret**, encrypted, and the instance names the key to read there. Open **Security → Secret Management**, then edit the secret named **Default**. Set two values, then press **Update Secret** : [![The default secret in its editor : the password of the API user default and the MongoDB connection string](https://docs.apimaker.dev/assets/images/screens/fa-secret.webp)](https://docs.apimaker.dev/assets/images/screens/fa-secret.webp) 1. `apiUserPasswords.default` : a password for the API user `default`. 2. `connectionString.mongodb` : how to reach your MongoDB. 3. **Update Secret**. ```typescript apiUserPasswords: { default: 'Choose_A_Password_1', // the password of the API user "default" }, connectionString: { mongodb: 'mongodb://user:password@127.0.0.1:27018/?authSource=admin&directConnection=true', }, ``` Where is the connection string of my MongoDB ? - **A MongoDB of Launchpad** : its card, on the **Databases** page. - **MongoDB Atlas** : **Connect → Drivers** on your cluster. - **Any other** : see [MongoDB connection strings](https://docs.apimaker.dev/v4/docs/Database-connection-string/mongodb-connection-strings.md). Every engine has [its page](https://docs.apimaker.dev/v4/docs/Database-connection-string/mysql-connection-strings.md). ## 2. Test the instance Open **APIs & Logic → Instance API (DB API)**. Pick the instance `mongodb`, press its edit button, then **Test Instance**. [![The instance mongodb : its connection is the key of the secret, Test Instance says the connection works](https://docs.apimaker.dev/assets/images/screens/fa-instance.webp)](https://docs.apimaker.dev/assets/images/screens/fa-instance.webp) 1. The key of the secret this instance reads. 2. **Test Instance** : it connects, then says so. **The connection works** : API Maker reached your MongoDB. Close the dialog. The database `shop` shows up after your first save : MongoDB makes a database and its tables on the first write. ## 3. Save a category, then a product Open **APIs & Logic → API Testing**. It calls any API of the account for you, with a token of the API user `default`. [![API Testing : the instance, database and table, the API Save single-multiple, its body, and SEND](https://docs.apimaker.dev/assets/images/screens/fa-request.webp)](https://docs.apimaker.dev/assets/images/screens/fa-request.webp) 1. The instance, the database and the table. 2. The API : **Save single-multiple**. 3. The **Body** : the row to save, in JSON. 4. **SEND**. A product belongs to a category, so the category comes first. 1. Pick the instance `mongodb`, the database `shop` and the table `categories`. 2. Pick the API **Save single-multiple**, open **Body**, and type : ```json { "name": "Accessories" } ``` 3. Press **SEND**. The answer shows the saved category. Copy its `_id`. 4. Now pick the table `products`, the same API, and this body. Paste the `_id` of the category : ```json { "name": " Wireless Mouse ", "currency": "in", "price_cents": 129900, "category": "the _id of Accessories" } ``` 5. Press **SEND**. [![The answer of the save : 201 Created, and the row as it was saved](https://docs.apimaker.dev/assets/images/screens/fa-saved.webp)](https://docs.apimaker.dev/assets/images/screens/fa-saved.webp) 1. `201 Created` : the row is saved. 2. The row as the database holds it : name trimmed, `currency` in capitals, a number and a slug added. Look at what the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) of `products` did on the way : - the spaces around the name are gone, and `currency` is in capitals ; - `status`, `is_active` and the dates got their default values ; - `product_no` got the next number of a counter, `public_id` a unique id, and `slug` a value made from the name. ## 4. Read it back Pick the API **Get all**. Open **Query Params** and fill two of them : | Param | Value | What it does | |---|---|---| | `select` | `name,slug,price_cents,category` | Only these fields. | | `deep` | `[{s_key:'category'}]` | Reads the category of each product, and puts it inside. | Press **SEND**. [![Get all with the query params select and deep, and the answer with the category inside each product](https://docs.apimaker.dev/assets/images/screens/fa-read.webp)](https://docs.apimaker.dev/assets/images/screens/fa-read.webp) 1. The API : **Get all**. 2. `select` : the fields you want. `deep` : the category of each product, read for you. 3. The products, each with its category. `deep` needs only the field : the schema knows that `category` points to the table `categories`. It works across tables, databases and even different kinds of databases : see [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) and [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md) for filters. ## 5. See a refusal Save a product that breaks the rules : a name too short, a currency the schema does not know, a negative price, a discount without approval. ```json { "name": "M", "currency": "EUR", "price_cents": -5, "category": "the _id of Accessories", "discount_pct": 10 } ``` [![A refused save : 400 Bad Request and one error for every broken rule](https://docs.apimaker.dev/assets/images/screens/fa-errors.webp)](https://docs.apimaker.dev/assets/images/screens/fa-errors.webp) 1. `400 Bad Request` : nothing was saved. 2. One error for each broken rule, all at once. API Maker answers `400` with every problem at once, and saves nothing. Your app can show these messages next to the right fields. ## 6. Call it from your app Your app does what API Testing did : it gets a token, then calls the URL with it. Here with `curl`. `$AM` is the address of API Maker : `http://127.0.0.1:38246` on your computer. Get a token for the API user `default`, with the password you typed in the secret : ```bash curl -s -X POST "$AM/api/system-api/admin/token" \ -H "Content-Type: application/json" \ -d '{ "u": "default", "p": "Choose_A_Password_1" }' ``` ```json { "success": true, "statusCode": 200, "data": { "token": "eyJhbGciOi…", "expires_in": 259200 } } ``` Then call **Get all** with the token in `x-am-authorization` : ```bash curl -s "$AM/api/schema/admin/mongodb/shop/products?select=name,price_cents" \ -H "x-am-authorization: eyJhbGciOi…" ``` The URL says everything : `/api/schema` (an API of a table) `/admin` (the account) `/mongodb/shop/products` (instance, database, table). **API Testing** writes this code for you in 21 languages : press its code button. > **Warning : The group Default allows everything** > > It is fine to learn, wrong for real users. Step 6 of this path, [Secure your APIs](https://docs.apimaker.dev/v4/docs/getting-started/secure-your-api.md), gives each app only what it needs. ## The rest of the sample - **Custom APIs** : open **APIs & Logic → Custom API**, folder `default`. Each one is a short example of a feature, with comments : deep populate, find and join, caching, WebSocket notifications, a login. - **Hooks** : the sample keeps each person to their own rows. - **Schemas** : the five tables use most features of a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md). --- # Your Own Code - a First Custom API > Write your first custom API in API Maker - one function behind a URL. Set its name, path and method, write the code with the global object g, save it, test it in API Testing, and call it from your app. Then see where else your code can run. Source: https://docs.apimaker.dev/v4/docs/getting-started/first-custom-api.html The ready-made APIs read and write your tables. When you need more, like a report, a total, or a call to another service, you write a **custom API** : one function behind a URL. You write it in the admin panel, and it works as soon as you save it. There is nothing to build or deploy. > **In short** > > - A custom API has two parts : **Basic Info** (its name, path, method, and who may call it) and **Code** (one function). > - Your function gets one object, `g` : the request in `g.req`, your databases in `g.sys.db`. > - What the function returns is the answer. Save it, then press **Test**. - Video: [Your First Custom API: From a Function to a Live Endpoint](https://apimaker.dev/videos/first-custom-api) (6 min) ## 1. Add a custom API Open **APIs & Logic → Custom API**, then press **+** (Add New). The dialog has three tabs : **Basic Info**, **Code** and **Docs**. [![A new custom API : its name, path, method and access in the Basic Info tab](https://docs.apimaker.dev/assets/images/screens/ca-dialog.webp)](https://docs.apimaker.dev/assets/images/screens/ca-dialog.webp) 1. Tabs : **Basic Info**, **Code**, **Docs**. 2. Its name, its path, its method, who may call it. **Basic Info** says where the API answers and who may call it. It is a few lines of TypeScript. Write these : **Basic Info** ```typescript import * as T from 'types'; let customApi: T.ICustomApiSettingsTypes = { name: 'Top Products', path: '/top-products', requestMethod: T.ERequestMethod.GET, apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, // callers need a token errorList: [], }; module.exports = customApi; ``` ## 2. Write the code Open the **Code** tab. The language is TypeScript : you can pick another one above the editor. [![The Code tab of a custom API : the language picker and the code of the function](https://docs.apimaker.dev/assets/images/screens/ca-code.webp)](https://docs.apimaker.dev/assets/images/screens/ca-code.webp) 1. The language and its runtime : TypeScript is the default. 2. Your function. `g` brings the request and every API of API Maker. Write a function named `main`. It reads the most expensive products of the sample shop, and returns their names and prices : **Code** ```typescript import * as T from 'types'; async function main(g: T.IAMGlobal) { const limit = Number(g.req.query.limit || 5); // ?limit=3 in the URL const products = await g.sys.db.query({ instance: 'mongodb', database: 'shop', collection: 'products', find: { is_active: true }, sort: '-price_cents', // most expensive first limit: limit, }); g.logger.log(`${products.length} products read`); return products.map((p: any) => ({ name: p.name, price: p.price_cents / 100, currency: p.currency })); } module.exports = main; ``` Type `g.` in the editor : it lists what `g` holds. It also knows your instances, tables and fields, and completes them as you type. ## 3. Save and test Press **Save**. Your API shows in the tree of the page. Press its **Test** button. [![The new custom API in the tree of the Custom API page, with its Test button](https://docs.apimaker.dev/assets/images/screens/ca-tree.webp)](https://docs.apimaker.dev/assets/images/screens/ca-tree.webp) 1. Your custom API, in its folder. 2. **Test** opens it in API Testing. **API Testing** opens on your API. Press **SEND**. [![The custom API in API Testing : SEND and its answer](https://docs.apimaker.dev/assets/images/screens/ca-test.webp)](https://docs.apimaker.dev/assets/images/screens/ca-test.webp) 1. **SEND** calls it. 2. The status and the time it took. 3. What your function returned. The answer is what `main` returned, inside the usual envelope : `{ "success": true, "statusCode": 200, "data": [ … ] }`. The **Logs** tab shows what `g.logger.log` wrote. Change the code, press **Save Code**, and press **SEND** again : the new code answers at once. ## 4. Call it from your app The URL of a custom API is `/api/custom-api//` : ```bash curl -s "$AM/api/custom-api/admin/top-products?limit=3" \ -H "x-am-authorization: $TOKEN" ``` The token is the one of [step 6 of Your first API](https://docs.apimaker.dev/v4/docs/getting-started/first-api.md#6-call-it-from-your-app). ## Where else your code can run A custom API runs when an app calls its URL. Your code can also run at other moments : | Code | It runs | Example | |---|---|---| | [Pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) | before an API | keep a person to their own rows | | [Post hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) | after an API | hide a field in the answer | | [Event](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.kotlin.md) | after something happened | an order is saved : send an email | | [Scheduler](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md) | on a timer | every night : clean old carts | | [WebSocket event](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.kotlin.md) | when your apps should hear of a change | a new order shows live on a screen | | [Utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.kotlin.md) | when your other code calls it | a price calculation used by ten APIs | | [Test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md) | when you run the tests | check that Top Products answers | ## In another language TypeScript, Python, PHP, C#, Go, Rust, Java and Kotlin all work. Pick the language above the editor : it offers a template. The [Hello world](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md#hello-world) of the custom APIs page shows the same function in all eight. More : [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md) (every setting, files, errors) · [the global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) · [code examples](https://docs.apimaker.dev/v4/examples/index.kotlin.md). --- # Secure Your APIs > Decide which app may call which API of API Maker - make a group of allowed APIs, an API user for each app, give it its groups, check a refused and an allowed call, choose public or private access, and let the security report check the whole account. Source: https://docs.apimaker.dev/v4/docs/getting-started/secure-your-api.html Every API of API Maker is locked : a call without a valid token gets `401`. You decide who gets a token, and what each token may call. You write no code for it. > **In short** > > - A **group** is a list of what may be called : APIs, tables, fields. > - An **API user** is one app : a username and a password. Give it one or more groups. > - The app trades its username and password for a **token**, and sends the token on every call. > - The **security report** reads the whole account and says what to fix. > Diagram : Two tokens, two gates : the API user (which application) and the person (which rows) > > Two identities on every request : the API user token from the token API in x-am-authorization tells which application calls, and the token of a person from a token generator in x-am-user-authorization tells who the person is. Groups of the API user decide which APIs, tables and fields may be called ; pre hooks decide which rows belong to the person. - Video: [API Authorization Without Auth Code: Access Types, API Users, Groups](https://apimaker.dev/videos/authorization-basics) (7 min) ## 1. Make a group Open **Security → API Group Permissions** and press **+**. Name the group after what it is for, for example `Support team`, then press **Save Group**. Pick the group, then go from left to right : the kind of API, the instance, the database, the table. [![API Group Permissions : from left to right, the group, the kind of API, the instance and database, then the tables](https://docs.apimaker.dev/assets/images/screens/sec-group.webp)](https://docs.apimaker.dev/assets/images/screens/sec-group.webp) 1. The groups. Pick one. 2. The kind of API : the APIs of your tables, custom APIs, system APIs. 3. The instance, then the database. 4. The tables. Then tick the APIs of each table it may call. Tick the APIs of each table the group may call : for example **Get all** and **Get by id** to read, and nothing to write. The **Schema Fields** column limits the fields it may read and write. Press **Save**. ## 2. Make an API user Open **Security → API User Permissions** and press **+**. One API user is one app : your website, your mobile app, a partner. [![A new API user : its name, username and the path of its password in the secret](https://docs.apimaker.dev/assets/images/screens/sec-api-user.webp)](https://docs.apimaker.dev/assets/images/screens/sec-api-user.webp) 1. A name : the app that will call. 2. Its username. 3. Where its password is kept in the secret. 4. **Save User**. The password is not typed here. It stays in your **default secret**, encrypted, and **Password Path** names its key. Open **Security → Secret Management** and add it : ```typescript apiUserPasswords: { default: 'Choose_A_Password_1', support_desk: 'a-long-password-for-the-support-desk', // the new API user }, ``` ## 3. Give it its groups Back on **API User Permissions**, pick the API user, tick its groups, and press **Save**. [![API User Permissions : an API user on the left, the groups it belongs to on the right](https://docs.apimaker.dev/assets/images/screens/sec-user-groups.webp)](https://docs.apimaker.dev/assets/images/screens/sec-user-groups.webp) 1. The API user. Pick it. 2. Tick the groups it belongs to. 3. **Save**. ## 4. Try it In **API Testing**, the **Header** tab has the row `x-am-authorization` : pick your new API user there. Call an API its groups do not allow : [![A call refused with 403 : no group of the API user allows this API](https://docs.apimaker.dev/assets/images/screens/sec-refused.webp)](https://docs.apimaker.dev/assets/images/screens/sec-refused.webp) 1. `403` : no group of this API user allows the call. 2. The reason. `403` : refused, with the reason. Call an API the group allows, and the data comes back. Your app does the same with a token : ```bash # 1. the token of the API user curl -s -X POST "$AM/api/system-api/admin/token" \ -H "Content-Type: application/json" \ -d '{ "u": "support_desk", "p": "a-long-password-for-the-support-desk" }' # 2. a call, with the token curl -s "$AM/api/schema/admin/mongodb/shop/customers" -H "x-am-authorization: " ``` ## 5. Public, private, or only for your code Each table and each custom API has an **access type** : | Access type | Who may call | |---|---| | `TOKEN_ACCESS` | Only callers with a token whose groups allow it. The default. | | `IS_PUBLIC` | Anyone, without a token : a public price list, a contact form. | | `NO_ACCESS` | Nobody from outside : only your own code and API Testing. | Set it in the [settings of the table](https://docs.apimaker.dev/v4/docs/settings/collectionSettings.md) or of the [custom API](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.md). ## 6. Let the security report check the account Open **Security → APIs Security Report & Actions**. It reads every setting, group, hook and line of code of the account, and gives a score out of 100. [![APIs Security Report and Actions : a score of 95 out of 100, and where the risks are](https://docs.apimaker.dev/assets/images/screens/sec-report.webp)](https://docs.apimaker.dev/assets/images/screens/sec-report.webp) 1. **Scan** the account again after a change. 2. The score, out of 100. 3. Where the risks are. Open one to see the finding and its fix. Each finding says what a person could reach, with the request that proves it. Most have a fix : look at the change, apply it, and the account is scanned again. - Video: [APIs Security Report: Find Data Leaks and Fix Them in One Click](https://apimaker.dev/videos/security-report) (12 min) ## People, not only apps An API user is an app. When people sign in to your app, each person can also get a token, and a pre hook keeps them to their own rows : see [the two gates](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.md#the-two-gates) and [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.kotlin.md). Before real users arrive, go through [Before you go live](https://docs.apimaker.dev/v4/docs/security/go-live.md) : the default API user and its group `Default`, which allows everything, must go. --- # Put API Maker on a Server > Install API Maker on your own Ubuntu server with the Cloud mode of API Maker Launchpad, over SSH from your computer - add the server, choose a layout, add the license key, review and deploy. HTTPS, the firewall, upgrades, bigger architectures and the files on the server. Source: https://docs.apimaker.dev/v4/docs/getting-started/install-on-server.html When your APIs are ready, they need a server that is always on. **Launchpad installs API Maker on your server for you**, from your computer, over SSH. You rent a server anywhere, and Launchpad does the rest : API Maker, its MongoDB and Redis, a reverse proxy with HTTPS, and the firewall. > **In short** > > 1. Rent an **Ubuntu 24.04** or **26.04** server, from any provider. > 2. In Launchpad, switch to **Cloud**, and add the server. > 3. Make a project, choose the layout **All in one**, add your license key, and press **Deploy**. - Video: [Launchpad: Deploy API Maker to Your Own Servers over SSH](https://apimaker.dev/videos/launchpad-cloud-mode) (7 min) ## What you need | | | |---|---| | A server | **Ubuntu 24.04 LTS** or **Ubuntu 26.04 LTS**, x86-64 or ARM64. To start : 1 CPU core, 1 GB of memory, 20 GB of disk. Any provider works : Hetzner, DigitalOcean, AWS, Linode… | | Its login | `root`, or a user with sudo : a password, a private key, or your SSH agent. | | A license key | One for each API Maker server. Write to [contact@apimaker.dev](mailto:contact@apimaker.dev). | | A domain (optional) | For HTTPS. Point it at the address of the server first. | Nothing is installed on your computer for this, and there is no agent and no third-party service : Launchpad talks to your server over SSH. > **Ubuntu 26.04 : update the kernel** > > The first kernels of Ubuntu 26.04 (Linux 7.0, before 7.0.0-31 of September 2026) make MongoDB 8 stop at start > ([SERVER-121912](https://jira.mongodb.org/browse/SERVER-121912)). Launchpad still runs it there, in a slower mode, and > says so when it checks the server. Update the kernel and restart the server : `apt update && apt full-upgrade -y`, > then `reboot`. MongoDB takes its faster mode back by itself. ## 1. Switch Launchpad to Cloud Press **Cloud** at the top right of Launchpad. The sidebar now lists your projects, your servers and what Launchpad did on them. [![Launchpad in Cloud mode : the Local and Cloud switch, the sidebar and a project with its environment](https://docs.apimaker.dev/assets/images/screens/cloud-mode.webp)](https://docs.apimaker.dev/assets/images/screens/cloud-mode.webp) 1. **Cloud** : Launchpad now works on your servers. 2. Your projects, servers and operations. 3. An environment : one API Maker with its servers. ## 2. Add your server Open **Servers** and press **Add a server**. Paste the email your provider sent : Launchpad fills in the form. [![Add a server : paste the email of your provider, and the form fills itself in](https://docs.apimaker.dev/assets/images/screens/cloud-add-server.webp)](https://docs.apimaker.dev/assets/images/screens/cloud-add-server.webp) 1. Paste the email your provider sent. 2. The name, address and user are filled in for you. 3. **Test the connection**, then **Save**. Press **Test the connection**. Launchpad checks the server : its system, its memory, its disk. The first time, it shows the fingerprint of the server's key : accept it, so a fake server is refused later. Press **Save the server**. The password or key is kept encrypted on your computer, its key in the keychain of your system. ## 3. Make a project and choose a layout On **Overview**, press **New project** and name it. A project holds environments : `production`, `staging`… Each environment is one API Maker, with its own servers. Press **Choose a layout** : [![Choose a layout : All in one, App and data, Dedicated databases, Load balanced](https://docs.apimaker.dev/assets/images/screens/cloud-layout.webp)](https://docs.apimaker.dev/assets/images/screens/cloud-layout.webp) 1. **All in one** : one server runs everything. Start here. 2. Bigger layouts, for later. 3. **Use this layout**. Pick **All in one** : one server runs everything. It is cheap, simple, and enough to start. Press **Use this layout**. The layout plans a server named `main` : connect it to your server from the **Servers** tab of the environment. ## 4. Add the license key and deploy Open the **Settings** tab of the environment. Check the version of API Maker, and paste the license key of the server. Then press **Review & deploy**. [![Review and deploy : what will be installed on every server, then Deploy](https://docs.apimaker.dev/assets/images/screens/cloud-review.webp)](https://docs.apimaker.dev/assets/images/screens/cloud-review.webp) 1. What will be installed, server by server. 2. **Deploy**. The review lists what will be installed, server by server. Nothing changes before you press **Deploy**. For `production`, Launchpad asks you to type its name first. [![The deployment, done : its stages and the output of every server](https://docs.apimaker.dev/assets/images/screens/cloud-deployed.webp)](https://docs.apimaker.dev/assets/images/screens/cloud-deployed.webp) 1. Deployed : API Maker runs on your server. The deployment shows each stage and the output of the server, live. It takes a few minutes. At the end, it shows the addresses of the admin panel and of the API. ## 5. Open the admin panel Open the address of the admin panel, and sign in as on your computer. Then, before anyone else uses it, go through [Before you go live](https://docs.apimaker.dev/v4/docs/security/go-live.md) : new passwords, HTTPS, and your own API users. To bring your work from your computer : your schemas, APIs and code travel with [Git](https://docs.apimaker.dev/v4/docs/Git/git.md). Secrets never travel : fill the default secret of the server on the server. ## HTTPS and addresses Apps reach API Maker through the **reverse proxy** of the server : Caddy by default, or Ferron, Pingap, or [the web server built into API Maker](https://docs.apimaker.dev/v4/docs/features/built-in-web-server.md) (API Maker 4.3.0 or newer). - **Without a domain**, it answers on the address of the server : the admin panel on port `80`, the API on `8080`, the WebSockets on `8081`. Plain HTTP : fine to try, not for real users. - **With your domains**, it answers over HTTPS. Its certificates come from Let's Encrypt and renew by themselves. Set the domains in the panel of the reverse proxy, then deploy. The firewall lets the web reach the reverse proxy only. SSH always stays open. ## What Launchpad sets up > Diagram : One server set up by API Maker Launchpad : the reverse proxy → systemd (API Maker : APIs, WebSockets, admin panel) → Docker (MongoDB, Redis, sandboxes) → your databases > > What API Maker Launchpad sets up on one Ubuntu server : a reverse proxy (Caddy, Ferron or Pingap) on ports 80 and 443 in front, API Maker run by systemd as the service api_maker_be (one program : the APIs and the WebSockets on port 38246, the admin panel on port 4626), MongoDB and Redis in Docker for the internal data of API Maker, sandbox containers for your code, and your own databases anywhere. - **API Maker** : one program, run by systemd as the service `api_maker_be`. systemd starts it at boot, and again if it stops. - **MongoDB and Redis** of API Maker, in Docker : its own data, not yours. - **The reverse proxy**, in Docker, and the **sandboxes** of your code. - **Your databases**, if you add some to the environment : MongoDB, PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, TiDB, with their replicas. ## Every day - **Health** checks every server : disk, memory, services, certificates. - **Logs**, **terminals** and **files** of every server, without SSH by hand. - **Operations** lists everything Launchpad did, with its output. - Video: [Launchpad Day Two: Upgrades, Health, Logs and Moving Data Between Servers](https://apimaker.dev/videos/launchpad-day-two) (8 min) ## Upgrade and roll back Open the **Versions** tab of the environment, pick another version of API Maker, newer or older, then **Review changes** and deploy. The new version is installed next to the one that runs, then API Maker switches to it. With several servers, they switch one after the other. You can also upgrade from the admin panel : [Deploy API Maker](https://docs.apimaker.dev/v4/docs/features/deploy-api-maker.md). ## Bigger setups Start on one server, and grow without changing your APIs. Each layout of Launchpad matches one of the [reference architectures](https://apimaker.dev/architectures) of the website : | Layout of Launchpad | What it is | |---|---| | **All in one** | API Maker, its MongoDB and Redis, and your database on one server. [Details](https://apimaker.dev/architectures/single-server) | | **App + data** | API Maker on one server, the databases on another. [Details](https://apimaker.dev/architectures/separate-database) | | **Dedicated databases** | API Maker, its own data, and your databases each on servers of their own. [Details](https://apimaker.dev/architectures/dedicated-data-tier) | | **Load balanced** | Several API Maker servers behind a gateway. Add servers at any time. [Details](https://apimaker.dev/architectures/load-balanced) | | One environment per stage | DEV, QA, UAT and PROD, each with its own servers and data. [Details](https://apimaker.dev/architectures/environments) | On the diagram of an environment, drag a part to another server, or **Shift**-click servers to cluster a database over them. The video below shows it. - Video: [Design Clusters on a Canvas: Replica Sets, Galera, Gateways and HTTPS](https://apimaker.dev/videos/launchpad-clusters) (8 min) ## The files on the server > **Where everything is** > > | Path | What it is | > |---|---| > | `/root/projects/sava_api_maker` | API Maker : its program, its uploads, `license.txt`. | > | `/root/config` | `.env` (its [configuration](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.md)), one Docker Compose file per service, the reverse proxy, the settings of each database. | > | `/root/docker-data` | The data of every container, the certificates too. | > | `/root/logs` | The logs of API Maker (`api_maker_be.txt`), of the reverse proxy and of every operation. | > | `/etc/systemd/system/api_maker_be.service` | The service of API Maker. | > > API Maker listens on port `38246` (its APIs and WebSockets), its MongoDB on `38248`, its Redis on `7479`. > **Tip : Moving a server of API Maker v3 ?** > > Launchpad moves it to v4 in place, and rolls back on failure : see [Upgrade from v3](https://docs.apimaker.dev/v4/docs/getting-started/upgrade-from-v3.md). --- # Words to Know > The words of API Maker explained in one or two simple sentences each - instance, schema, schema and generated APIs, custom API, hooks, events, schedulers, sandbox, secret, API user, group, token, environment and more. Source: https://docs.apimaker.dev/v4/docs/getting-started/glossary.html Each word of API Maker in one or two sentences, with the page that tells more. The words are in the order you meet them. ## The basics API : An address (a URL) that an app calls to read or change data. API Maker answers in JSON : `{ "success": true, "statusCode": 200, "data": … }`. API Maker : The program that serves your APIs. It runs on your computer or on your servers. [How it works](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.md) API Maker Launchpad : A free desktop app that installs and runs API Maker : on your computer (**Local**) or on your servers (**Cloud**). [Install API Maker](https://docs.apimaker.dev/v4/docs/getting-started/local-run.md) Admin panel : The website where you set up everything : databases, tables, code, security. API Maker serves it. [The admin panel](https://docs.apimaker.dev/v4/docs/getting-started/admin-panel.md) Account and user path : An admin account owns its databases, APIs and code. Its **user path**, like `admin`, is part of every URL of its APIs. A team adds [developer accounts](https://docs.apimaker.dev/v4/docs/features/team-work.md#developer-accounts). Root user : The user who manages the installation itself : the admin accounts and the settings of every server. You rarely need it. ## Your data Instance : One database server that API Maker connects to, like `mongodb` or `postgres`. Its connection string stays in the secret. [Instances](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#instances) Table, collection : A table of your database. MongoDB calls it a **collection** : the docs use both words. Schema : The description of a table : its fields, their types, their rules, their default values, and the links to other tables. [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) Schema APIs, generated APIs : The ready-made APIs of every table. **Schema APIs** (`/api/schema/…`) check each write against the schema. **Generated APIs** (`/api/gen/…`) do not. [All the APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) Query params : The options of a read : `find` (which rows), `select` (which fields), `sort`, `limit`, `deep`. [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md) Deep populate : Reading the rows a field points to, in the same call : an order with its customer, inside. [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) Table Designer : A page to make and change tables with the mouse, on any database. [Table designer](https://docs.apimaker.dev/v4/docs/schema/table-designer.md) Migration script : Code that changes a database once, on each environment : add a column, fill it. [Database migration](https://docs.apimaker.dev/v4/docs/features/database-migration.kotlin.md) ## Your code Custom API : Your own function behind a URL, in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin. [Your own code](https://docs.apimaker.dev/v4/docs/getting-started/first-custom-api.md) The global object `g` : What your code receives : the request (`g.req`), the answer (`g.res`), and every API of API Maker (`g.sys`). [Global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) Pre hook, post hook : Code that runs **before** an API (it can change the request) or **after** it (it can change the answer). [Pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) Event : Code that runs after something happened, like an order saved. [Events](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.kotlin.md) Scheduler : Code that runs on a timer : every five minutes, every night. [Schedulers](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md) WebSocket event : A live message to your apps when data changes, without asking again and again. [WebSocket events](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.kotlin.md) Utility class : Code shared by your other code : write a price calculation once, use it in ten APIs. [Utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.kotlin.md) Test case : Code that calls your APIs and checks their answers. [Test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md) Sandbox : A Docker container that runs your code, apart from API Maker. Each language has its own. [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) Native process : Code that runs on the machine itself, next to API Maker, without Docker. Faster, less isolated. [Native process](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md#native-process) API Testing : A page of the admin panel that calls any API for you, and shows the answer. [Your first API](https://docs.apimaker.dev/v4/docs/getting-started/first-api.md) ## Security Secret : Your passwords and connection strings, kept encrypted by API Maker. They never go to Git. [Secrets](https://docs.apimaker.dev/v4/docs/secrets/secrets.kotlin.md) API user : The login of one app : a username and a password. [API user permission](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.md) Group : The list of what an API user may call : APIs, tables, fields. [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) Token : What an app sends on every call, in the header `x-am-authorization`. It gets it from the [token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.kotlin.md) with its username and password. Auth provider : Turns the sign-in of a person of your app into a second token, so your code knows who calls. [Sign-in providers](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.md) Access type : Who may call an API : `TOKEN_ACCESS` (with a token, the default), `IS_PUBLIC` (anyone), `NO_ACCESS` (only your own code). [Secure your APIs](https://docs.apimaker.dev/v4/docs/getting-started/secure-your-api.md#5-public-private-or-only-for-your-code) Security report : A page that reads your whole account, says where data could leak, and fixes it. [Security report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.md) ## Running it Environment : One API Maker with its own servers and data : DEV, QA, PROD. [Put it on a server](https://docs.apimaker.dev/v4/docs/getting-started/install-on-server.md) Reverse proxy : The program in front of API Maker on a server, for HTTPS : Caddy, Ferron, Pingap, or the [web server built into API Maker](https://docs.apimaker.dev/v4/docs/features/built-in-web-server.md). Local client : Keeps a folder of your computer in sync with an account, so you can code in your own editor. [Developer tools](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#your-own-editor) Git integration : Your schemas, APIs and code as files in Git : commit them, then pull them on another server. [Git integration](https://docs.apimaker.dev/v4/docs/Git/git.md) Caching : Answers kept in Redis, so the same read is fast the next time. [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.md) Logs : What API Maker records about each call, to find a problem. [Logs and dashboards](https://docs.apimaker.dev/v4/docs/logs/logs-and-dashboards.md) --- # Upgrade from API Maker v3 > Move a server of API Maker v3 (the Node.js edition) to v4 in place with the V3 to V4 Migration page of API Maker Launchpad, which checks first and rolls back on failure - what stays, what changes for your apps and your code, and what to check after. Source: https://docs.apimaker.dev/v4/docs/getting-started/upgrade-from-v3.html API Maker v4 takes over a server of v3 where it is. The data of MongoDB and Redis stays, and so do the secrets, the license key and the addresses of your APIs. **API Maker Launchpad** does the move for you, and puts everything back as it was if a step fails. > **In short** > > 1. In Launchpad, switch to **Cloud** and open **V3 to V4 Migration**. > 2. Pick your server : Launchpad checks it first and changes nothing. > 3. Choose where it goes, read the review, and press **Migrate to API Maker v4**. - Video: [Upgrade from API Maker 3 to 4: What Changes in Your Code, Branch and Database](https://apimaker.dev/videos/upgrade-from-v3) (6 min) ## Before you start - **Try it on a copy first** when the server is in production : a snapshot of the server, restored on a new one. - **Read the breaking changes** of [v4.0.0](https://docs.apimaker.dev/v4/docs/blog/release-notes/v4.0.0.md) : a few queries match differently (`null` and `$like`), and a value starting with `__:` is no longer SQL. - **One server at a time.** Launchpad refuses a secondary server of v3, and a server whose Caddy spreads requests over other servers, and says so. ## With API Maker Launchpad - Video: [Move an API Maker 3 Server to v4 in Place, with API Maker Launchpad](https://apimaker.dev/videos/move-a-v3-server-with-launchpad) (8 min) **V3 to V4 Migration** is in the sidebar of the **Cloud** mode of [API Maker Launchpad](https://docs.apimaker.dev/v4/docs/getting-started/local-run.md#cloud-mode). [![V3 to V4 Migration : the checks of a server of API Maker v3](https://docs.apimaker.dev/assets/images/screens/v3-check.webp)](https://docs.apimaker.dev/assets/images/screens/v3-check.webp) 1. **V3 to V4 Migration**, in Cloud mode. 2. The checks : what runs on the server, and whether it can move. 3. What API Maker v3 runs with today. 1. **Check** Pick the server, or add it with its SSH login. Launchpad reads it without changing anything : the version of v3 and what runs it, MongoDB, Redis, Caddy, the license and the free disk. It says what prevents the move, and what to do. 2. **Destination** Choose a project and an environment of Launchpad, new or empty, and the version of API Maker v4. 3. **Review** Every step, the lines of `.env` that change, the Caddyfile before and after, and the checks at the end. Confirm by typing the name of the server. 4. **Watch it run** The migration runs on the server itself : closing the app does not stop it, and the log follows live. **Stop** rolls back. What the migration does, in order : 1. Downloads API Maker v4 next to v3, and checks it, while v3 still serves. 2. Saves what v3 runs with (`.env`, the Caddyfile, the services) in `/root/backups/api_maker_v3_to_v4/`. 3. Stops v3, then saves the data of MongoDB while nothing writes to it. 4. Keeps v3 in `/root/projects/sava_api_maker_v3`, and puts v4 in `/root/projects/sava_api_maker` with the license and the uploaded files of v3. 5. Starts v4 with systemd, and points Caddy at it : the same addresses and certificates. 6. Checks v4 the way your apps reach it : an API, the WebSockets and the admin panel. **When a step fails, or you press Stop, everything goes back as it was**, the data of MongoDB included, and v3 starts again. The folder of v3 and the backup stay on the server until you delete them. Once it is done, the server is an environment of Launchpad, like any other : health, logs, terminals and upgrades. ## What changes for your apps - **WebSockets** move to the port of the APIs : a client which reached `:38245` directly connects to `:38246`. Behind Caddy, the addresses stay the same. - **The admin panel** is served by API Maker on port `4626`, or through Caddy as before. - **Tokens** of your API users and of your users keep working : v4 reads the same secrets. - **`x-am-meta`** is answered to the admin panel only (its API Testing page). See [x-am-meta](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-meta). ## What changes for your code - **Packages.** A sandbox of v4 has only the packages its account lists in [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#packages). The packages v3 had built in (`lodash`, `moment`…) are added to that list by the first start of v4. - **Removed features.** UI Maker, the API Maker store, third party APIs and notes are gone. A Git pull lists what it skipped. - **Queries.** `null` in `find` on SQL means `IS NULL`, `$like` on MongoDB is the pattern of SQL, and `__:` values are plain values : see the [breaking changes](https://docs.apimaker.dev/v4/docs/blog/release-notes/v4.0.0.md). - **Languages.** Your TypeScript and JavaScript run as before. New code can be written in [seven more languages](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md). ## After the move - Open the admin panel and run the [security report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.md) : it reads the hooks and groups of v3 and says where rows are open. - Run your [test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md), or call the APIs your apps use most. - Go through [Before you go live](https://docs.apimaker.dev/v4/docs/security/go-live.md). - Delete the folder of v3 and the backup once you are sure. --- # API Maker Cheat Sheet > All of API Maker on one page - URLs, tokens, headers, query params, find operators, answers and status codes, schema rules, custom APIs, hooks, events, schedulers and cron, WebSockets, locks, the g object in 8 languages, security, settings, ports, Git and the admin panel. Source: https://docs.apimaker.dev/v4/docs/cheat-sheet.html - [Call an API](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md#call) - [Query and answer](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md#query) - [Schema](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md#schema) - [Your code](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md#code) - [The object g](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md#g) - [Events, jobs, live data](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md#events) - [Security](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md#security) - [Run it](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md#operate) - [Admin panel](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.md#panel) ## Call an API ### The 17 operations of a table [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) | Operation | Method | After `…/` | Body | Id, after `SCHEMA_` or `GEN_` | |---|---|---|---|---| | Get all | GET | `/` | query params | `GET_ALL` | | Get all by stream | GET | `/stream` | query params | `GET_ALL_STREAM` | | Get by id | GET | `/get-by-id/:id[/:primaryKey]` | query params | `GET_BY_ID` | | Query for get data | POST | `/query` | `{ find, select, sort, skip, limit, deep, getTotalCount }` | `POST_QUERY` | | Query by stream | POST | `/query-stream` | the same as query | `POST_QUERY_STREAM` | | Count | POST | `/count` | `{ find }` | `POST_COUNT` | | Distinct | GET | `/distinct/:field[/:order]` | | `GET_DISTINCT` | | Distinct with query | POST | `/distinct/:field[/:order]` | `{ find }` | `POST_DISTINCT_QUERY` | | Aggregate (MongoDB) | POST | `/aggregate` | a pipeline array | `POST_AGGREGATE` | | Save single or multiple | POST | `/save-single-or-multiple` | an object or an array | `POST_BULK_INSERT` | | Master save | POST | `/master-save` | objects with their related objects | `MASTER_SAVE` | | Update by id | PUT | `/update-by-id/:id[/:primaryKey]` | the fields to change | `PUT_UPDATE_BY_ID` | | Update many | PUT | `/update-many` | `{ find, updateData }` | `UPDATE_MANY` | | Replace by id (MongoDB) | PUT | `/replace-by-id/:id[/:primaryKey]` | the whole document | `PUT_REPLACE_BY_ID` | | Array operations (MongoDB) | PUT | `/array-operations` | `{ find, operations }` | `ARRAY_OPERATIONS` | | Remove by id | DELETE | `/:id[/:primaryKey]` | | `DEL_DELETE_BY_ID` | | Remove by query | POST | `/query/delete` | `{ find }`, never empty | `POST_QUERY_DELETE` | The path goes after `/api/schema////
`, or the same under `/api/gen`. MongoDB has all 17. MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB and Percona have 14. The schema APIs exist once a table has a schema. `:primaryKey` names another column to use as the key. The ids name the APIs in groups, hooks, events and WebSocket subscriptions : `SCHEMA_GET_ALL`. ### Request headers [Request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) | Who calls, how it runs | Value | |---|---| | `x-am-authorization` | token of the API user (the app) | | `x-am-user-authorization` | token of a person, from your users table | | `x-google-authorization`
`x-azure-authorization`
`x-aws-authorization`
`x-custom-authorization` | token of a person, from that provider | | `x-am-tenant-username` | tenant of a multi-tenant instance : `acme` | | `x-am-sandbox-timeout` | milliseconds the code may run, 13000 by default | | `x-am-run-in-sandbox` | `0` any sandbox, `1` the first one, `n` one of the first n | | `x-am-run-in-debugger` | `true` : the code stops at breakpoints, while debugging is on | | `x-am-cache-control` | `no_action` `reset_cache` | | `x-am-encrypted-payload` | `true` when the body is `{ dataEncFE }` | | The answer | Value | |---|---| | `x-am-response-case` | `noChange` `camelCase` `capitalCase` `constantCase` `dotCase` `headerCase` `noCase` `paramCase` `pascalCase` `pathCase` `sentenceCase` `snakeCase` | | `x-am-content-type-response` | `application/json` `text/xml` `text/yaml` `text/plain` `text/html` `application/octet-stream` | | `x-am-response-object-type` | `no_action` `make_flat` : `state_id_country_name` | | `x-am-get-encrypted-data` | `no_encryption` `get_only_encryption` `get_data_and_encryption` | | `x-am-internationalization` | a language of i18N Management : `Hindi` | | `x-am-meta` | `true` : time, groups and server in `meta`, in the admin panel only | | `x-no-compression` | `true` : never compressed (else from 51200 bytes) | | `accept-encoding` | `gzip`, else `br`, else `deflate` | ### URLs [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) | | | |---|---| | Schema APIs | `/api/schema////
` | | Generated APIs, no schema | `/api/gen////
` | | Custom APIs | `/api/custom-api//` | | System APIs, all `POST` | `/api/system-api//` | | A tenant | `::` in the path, or the header `x-am-tenant-username` | | WebSocket | `wss:///?x-am-authorization=` | | SSR website | `/ssr//`, or its own domain | `` is the API path of the account : `admin` for the first admin. `` is the database. ### Tokens [Get token](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.kotlin.md) ```text POST /api/system-api/admin/token { "u": "default", "p": "…" } the app { "name": "users_tg", "u", "p" } a person { "refresh_token": "…" } renew → { token, refresh_token, expires_in } ``` | | | |---|---| | `x-am-authorization` | the app : an API user. Always. | | `x-am-user-authorization` | a person of your users table | | `x-google-authorization`
`x-azure-authorization`
`x-aws-authorization`
`x-custom-authorization` | a person signed in with Google, Azure AD, AWS Cognito or your own provider | A token lives 259200 s (3 days), unless `expiresInSeconds` says otherwise. Its refresh token works 900 s longer. ### Who may call [Secure your APIs](https://docs.apimaker.dev/v4/docs/getting-started/secure-your-api.md) | Access type | Who may call | |---|---| | `TOKEN_ACCESS` | a token whose groups allow the API. The default. | | `IS_PUBLIC` | anyone, without a token | | `NO_ACCESS` | only your own code and API Testing | Set it as `apiAccessType` in the settings of a table or a custom API. A call passes when a group of the API user grants the API. With a person token, a group of the person must grant it too. ### System APIs [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) | | | |---|---| | Security | `/token` `/encrypt-data` `/decrypt-data` `/hash-data` `/get-secret-by-name` | | Redis and cache | `/get-redis-key` `/set-redis-key` `/remove-redis-key` `/reset-redis-cache-db` `/reset-redis-cache-custom-apis` `/reset-redis-cache-system-apis` | | Databases | `/get-table-meta` `/create-indexes` `/drop-indexes` `/get-indexes` `/execute-plain-query` `/multi-tenant-instance-updated` | | Events | `/emit-event` `/emit-event-ws` | | Checks | `/is-valid-data-for-table` `/is-valid-data-for-custom-api` `/is-valid-connection-string` | | Outside | `/call-external-api` | All are `POST …/system-api//…`. From code they are `g.sys.system.*` and `g.sys.cache.*` : `/token` is `getToken`, `/get-redis-key` is `cache.getKey`. Their id is the path in capitals (`ENCRYPT_DATA`), but `/token` is `GET_TOKEN` and `/get-secret-by-name` is `GET_SECRET`. ## Query and answer ### Query params [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md) | | | |---|---| | `find={status:'ACTIVE'}` | filter in JSON5, with MongoDB operators, on every database | | `select=name,price` · `select=-password` | only these fields · all but these | | `sort=-created_at,name` | `-` : descending | | `skip=20&limit=10` | paging : add a `sort` | | `getTotalCount=true` | adds `totalCount`, before paging | | `deep=[{s_key:'category'}]` | the related rows, in the same call | | `customer_id=42` · `price>=100` · `name=Bob,Alice` | any column is a filter : `>` `>=` `<` `<=` `!=`, lists | | `find={'owner.customer_id':42}` | find and join : a field of a related table | | `upsert=true` | update or replace by id : insert an unknown id | | `returnDocument=before` | update or replace by id : answer the row before | | `throwErrorIfRecordNotFound=true` | update, replace or remove by id : `404` instead of `data: null` | The query APIs take the same keys in their body. ### find operators [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md) | | | |---|---| | Compare | `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` `$in` `$nin` | | Logic | `$and` `$or` `$nor` `$not` | | Element | `$exists` · `$type` (MongoDB) | | Text | `$like: 'Bob%'` · `$regex` with `$options` · `$text` (MongoDB) | | Others | `$mod` `$expr` · `$isNull` (SQL) · `$where` `$jsonSchema` (MongoDB) | | Arrays (MongoDB) | `$all` `$elemMatch` `$size` | | Bits | `$bitsAllSet` `$bitsAnySet` `$bitsAllClear` `$bitsAnyClear` | | Geo (MongoDB) | `$geoWithin` `$geoIntersects` `$near` `$nearSphere` | SQL databases get the same filter, translated. `$like` : `%` is any text, `_` one character, on every database. ### deep [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) ```js deep: [{ s_key: 'customer_id', // the field here t_col: 'customers', // the other table t_key: 'id', // default : its key t_db, t_instance, // when they differ isMultiple: true, // an array of rows find, select, sort, skip, limit, deep: [ … ], // the next level }] ``` With a relation in the schema, `s_key` alone is enough. One query per deep item, for all the rows. ### What a write answers [returnData](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#return-data) | | | |---|---| | Save, master save | the saved rows ; with `?returnData=false`, only `{ savedRowsCount }` | | Update many | `{ updatedRowsCount }` ; with `"returnData": true`, also `updatedRows` | | Remove by query | `{ deletedRowsCount }` ; with `"returnData": true`, also `deletedRows` and `ids` | From code, `returnData` is a key of the call (`return_data` in Python and Rust). ### The answer [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md) ```json { "success": true, "statusCode": 200, "data": [], "totalCount": 5, "warnings": [], "logs": [], "meta": {} } { "success": false, "statusCode": 400, "errors": [ { "type": "required", "field": "name", "message": "…", "code": 400, "dataIndex": 0 } ] } ``` | | | |---|---| | `data` | the rows, the row, or what your code returns | | `logs` | the `g.logger` lines of the code which ran | | `meta` | with `x-am-meta: true`, admin panel only | ### Status codes [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md) | | | |---|---| | `200` | done | | `201` | created : save, master save | | `400` | wrong request : validation, bad query, no body | | `401` | token missing, malformed or expired | | `403` | the token is fine, no group grants it ; or the origin is not allowed | | `404` | no such path ; an unknown id with `throwErrorIfRecordNotFound` | | `500` | an error of your code or of the database | | Response header | | |---|---| | `x-am-data-source` | `cache` or `api` | | `x-am-request-id` | the id of the request in the logs | | `content-encoding` | `gzip`, `br` or `deflate` | ### Messages to know [Error messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) | | | |---|---| | `AM_00001` | a field is missing or of the wrong type | | `AM_00034` | `403` : no group of the API user grants the API | | `AM_00035` | `401` : `x-am-authorization` is not valid | | `AM_00092` | the person token is missing | | `AM_00056` | no schema on the table : call `/api/gen` | | `AM_00117` | the row changed since you read it : version mismatch | | `AM_00119` | record not found, with `throwErrorIfRecordNotFound` | | `AM_00032` | the origin of the browser is not allowed | Every message comes in the language of `x-am-internationalization`. ## Schema ### A schema file [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) ```typescript import { EType, ISchemaType, ISchemaProperty, } from 'types'; const schema: ISchemaType = { name: EType.string, // a type alone price: { __type: EType.number, validations: { required: true, min: 0 }, }, tags: [EType.string], // an array address: { city: EType.string }, }; module.exports = { schema }; ``` Only the schema APIs read it. Nested objects and arrays are for MongoDB : SQL tables are flat. A key the schema does not have is refused. ### Types [Data types](https://docs.apimaker.dev/v4/docs/schema/data-types.md) | `__type` | Holds | |---|---| | `EType.string` | text ; also a JSON column | | `EType.number` | numbers | | `EType.boolean` | `true`, `false` | | `EType.date` | dates, ISO strings in JSON | | `EType.objectId` | a MongoDB ObjectId | | `EType.file` · `EType.files` | uploads, in the request schema of a custom API | | `[EType.number]` | a vector column | | Other values | In JSON | |---|---| | Vector | `[0.12, 0.5, 0.33]` | | Bytes | `{ "type": "Buffer", "data": [1, 2] }` | | Shape | GeoJSON in ; the form of the driver out | | Interval | `{ "days": 1, "hours": 2 }` | | PostgreSQL array | `[1, 2, 3]` | ### Conversions [Schema](https://docs.apimaker.dev/v4/docs/schema/schema.md#conversions) | | | |---|---| | `trim` `trimStart` `trimEnd` | removes the spaces around | | `toLowerCase` `toUpperCase` | changes the case | | `conversionFun: (value, row) => …` | your function : it runs even when the field is absent | | `defaults: { defaultValue }` | a value for a missing field | | `defaults: { defaultFun: () => … }` | the same, computed ; `defaultValue` wins | | `shouldReplaceNullWithDefault` · `shouldReplaceEmptyStringWithDefault` | in `defaults` : `null` or `""` get the default too | | `encryption: true` | stored encrypted (AES, keys of the secret) | | `hashing: true` | stored as an HMAC SHA-256 hash | The keys of `conversions`. They run before the validations, on save and on update. ### Validations [Schema](https://docs.apimaker.dev/v4/docs/schema/schema.md#validations) | `validations` | Error `type` | |---|---| | `required: true` | `required` | | `min` · `max` | `min` · `max` | | `minLength` · `maxLength` | `minLength` · `maxLength` | | `email: true` | `emailNotValid` | | `enum: [ … ]` | `enumValidation` | | `unique: true` | `unique` | | `validatorFun: (value, row) => true` | `invalidValue` | A refused save answers `400` with every problem at once : `{ type, field, message, code, dataIndex }`. ### Keys and generated values [Schema](https://docs.apimaker.dev/v4/docs/schema/schema.md#keys-and-generated-values) | | | |---|---| | `isPrimaryKey: true` | the id of get, update and remove by id | | `isAutoIncrementByDB: true` | the database counts (SQL identity) | | `isAutoIncrementByAM: { start, step }` | API Maker counts, on every database ; `true` is 1, 1 | | `isAutoGenerateByAM: { valueGeneratorType }` | `ObjectID` `GUID_UUID` `ULID` `ShortUUID` | | `isConcurrencyControlField: true` | the version field : a stale update gets `400` | ### Relations [Schema](https://docs.apimaker.dev/v4/docs/schema/schema.md#relations-and-virtual-fields) ```typescript // orderItems.order_id → orders._id order_id: { __type: EType.objectId, database: 'shop', collection: 'orders', column: '_id', }, // orders.items : its rows of orderItems items: { __type: EType.objectId, isVirtualField: true, database: 'shop', collection: 'orderItems', s_columnVirtualLinker: '_id', t_columnVirtualLinker: 'order_id', }, ``` Add `instance` for another instance ; `table` works as `collection`. Relations feed `deep`, master save and the ER diagram. ## Your code ### A custom API [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md) ```typescript // Basic info : TypeScript in every language let customApi: T.ICustomApiSettingsTypes = { name: 'Hello World', path: '/hello-world', requestMethod: T.ERequestMethod.GET, apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, errorList: [], }; module.exports = customApi; ``` It answers at `/api/custom-api//hello-world`, with the exact path and method. What `main` returns is `data`. ### main in every language [Languages](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) | | | |---|---| | TypeScript | `async function main(g: T.IAMGlobal)` then `module.exports = main;` | | Python | `async def main(g: G):` | | PHP | `function main(AmG $g)` | | C# | `public static async Task Main(G g)` in `file static class Api` | | Go | `func Main(g *am.G) (any, error)` in `package code` | | Rust | `pub fn main(g: &mut G) -> Result` | | Java | `static Object main(G g)` in `class Api` | | Kotlin | `fun main(g: G): Any?` | Hooks, listeners, schedulers and initializers are the same function. ### Refuse a call [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md#errors-and-status-codes) | | | |---|---| | TypeScript | `throw [{ code: 400, message }]` | | Python | `raise Errors(message, code=400)` | | PHP | `throw new AmErrors($message, 400)` | | C#, Java | `throw new AmErrors(message, 400)` | | Kotlin | `throw AmErrors(message, 400)` | | Go | `return nil, am.Fail(400, message)` | | Rust | `Err(am::fail(400, message))` | The answer : `success: false`, the message in `errors`, the code as status. Any other error answers `500`. To send your own data with a status, set `g.res.statusCode`. ### Custom API settings [Settings](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.md) | | | |---|---| | `reqBodySchema` · `reqQueryParametersSchema` | converted and checked before the code runs | | `enableCaching` | the answer is kept in Redis | | `resetCacheOnModificationOf` | `'DB:instance:database:table'` · `'CA:name'` | | `authProviders` | the person tokens it needs ; `[]` : the API user only | | `acceptOnlyEncryptedData` | plain bodies are refused | | `customApiTimeoutInSeconds` | time of the code and its hooks ; 13 s by default | | `runOnNativeProcess` | runs on the machine of the server, not in Docker | | `separateSandboxSettings` | a sandbox of its own | | `fileUpload` | `enable`, the fields, sizes and extensions | | `errorList` | your messages, for i18N | ### Files and other answers [Files](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md#files) **Upload** : `multipart/form-data`, field `files` (up to `files41`). The code gets `g.req.body.files` : `originalname`, `mimetype`, `size`, `path`. 512 MiB per form by default. **Download** : return the `__am__` keys. ```typescript return { __am__downloadFilePath: 'r.pdf', __am__downloadFolderFileName: 'May.pdf', __am__cleanupFileOrFolderPaths: 'r.pdf', }; ``` Several paths in `__am__downloadFileOrFolderPaths` : one zip. **Other content** : set `g.res.contentType` and return the text, or bytes as base64. Headers of the answer : `g.res.headers`. ### Shared code [Utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.kotlin.md) | | | |---|---| | Utility class | code your other code imports : `import * as utils from 'utils/MyClass'` in TypeScript | | Process initializer | runs when a sandbox process starts, before any API : subscribe, warm a cache | | Migration script | changes a database once on each environment, after a Git pull | | `g.shared` | values of one runner, for all its requests | | `g.res.shared` | values of one request : pre hook → API → post hook | ### Hooks [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) | | | |---|---| | Where | an instance, a database, a table, one API, a custom or a system API | | Order | pre : instance → database → table → API. Post : the other way. | | `groupNames` | runs only for API users of these groups | | Language | the language of its API | | From code | `g.sys.db` calls skip the hooks ; `skipHookRunning: false` runs them | | Cache | an answer from the cache runs no hook | | A pre hook | Then | |---|---| | changes `g.req.query`, `body`, `headers` | the API gets the change | | returns a value | it is the answer : the API does not run | | throws | the call is refused | | A post hook | Then | |---|---| | changes `g.res.output` | the answer changes | | returns a value | it is the answer ; later post hooks do not run | | throws | an error answer ; the write stays done | `g.req.isApiRequestFromUser` is `false` for a call of your own code. ### Test cases [Test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md) ```typescript import * as assert from 'node:assert'; module.exports = [{ name: 'orderCount counts the orders', mocks: [{ method: 'db.getAll', when: [{ collection: 'orders' }], return: [{ id: 1 }, { id: 2 }] }], code: async (g: T.IAMGlobal) => { const out = await g.sys.test .runCustomApi({ name: 'orderCount' }); assert.deepStrictEqual(out, { count: 2 }); }, }]; ``` `npm run test` in the folder of the local client : exit `0` passed, `1` failed, `2` not complete. The coverage goes to `coverage/lcov.info`. On the page, Cmd+E or Ctrl+E in the editor runs it. ## The object g ### g in every language [Languages](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) ```kotlin g.req.body g.req.query g.req.params g.req.headers g.req.eventData g.req.reqInfo g.req.isApiRequestFromUser g.req.scheduler.getLong("worker") g.req.auth.getObject("authAMUser") "authAMDB" "authGoogle" "authAzure" "authAWS" "authCustom" g.res.output g.res.statusCode g.res.contentType g.res.headers g.res.errors g.res.warnings g.res.shared g.sys.db.getAll(GetAll("instance", "database", "collection").find(…).limit(…)) getAllByStream getById saveSingleOrMultiple masterSave arrayOperations updateById updateMany replaceById removeById query queryByStream removeByQuery aggregate count distinct distinctQuery g.sys.db.gen.getAllGen(…) the same, schemaless, with the Gen suffix g.sys.system.encrypt decrypt hash getToken callExternalApi executeQuery getSecret getTableMeta createIndexes dropIndexes getIndexes emitEvent emitEventWS isValidDataForTable isValidDataForCustomAPI multiTenantInstanceUpdated isValidConnectionString getLock releaseLock g.sys.cache.getKey setKey removeKey resetCacheDB resetCacheCustomApis resetCacheSystemApis g.logger.debug log info warn error g.res.shared["x"] = … travels with the request : pre hook → API → post hook g.shared["x"] = … lives as long as the runner, shared by the requests it serves g.sys.db.full.count(…) full : the whole envelope, no throw throw AmErrors("Bad id.", 400) refuse : 400, the message in errors Tax.add(100.0) utility classes ``` ### g.req [Global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) | | | |---|---| | `body` `query` `params` `headers` | the request ; a pre hook may change it | | `body.files` | the uploaded files | | `auth` | who calls : `authAMUser`, `authAMDB`… | | `eventData` | the data of an event, in a listener | | `scheduler` | `worker` of `workers`, `firedAt`, in a scheduler | | `reqInfo` | `url`, `apiCategory`, `reqMethod`, `apiInfo.id` | | `isApiRequestFromUser` | `false` for a call of your code | ### g.res [Global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md#gres) | | | |---|---| | `statusCode` | `200`… set it to answer another status | | `contentType` | `JSON` `XML` `YAML` `TEXT` `HTML` `OCTET_STREAM` | | `output` | the answer : a post hook reads and changes it | | `headers` | headers to add to the answer | | `errors` · `warnings` | the lists of the answer | | `shared` | values for the rest of this request | ### g.sys [Code examples](https://docs.apimaker.dev/v4/examples/index.kotlin.md) | | | |---|---| | `db.*` | the schema APIs of any table | | `db.gen.*` | the same, schemaless : `getAllGen`… | | `system.*` | the system APIs, `getLock`, `releaseLock` | | `cache.*` | your keys in Redis, the cache resets | | `test.*` | in a test case : `runCustomApi`, `mock`, `clearMocks`, `calls` | A call takes the `headers` and `queryParams` of an HTTP call. It returns `data` and throws the errors. Ask for the whole answer with `true` last (TypeScript) or through `full` (the other languages). ### g.logger, g.shared [Global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md#glogger-and-gshared) | | | |---|---| | `g.logger.log(…)` | also `debug` `info` `warn` `error` : one line, in `logs` of the answer and in the log profiles | | `g.shared` | lives as long as the runner : a cache, never the only copy | | `g.res.shared` | lives for one request | | `console.log` | not kept on the native process : use `g.logger` | ## Events, jobs, live data ### Events [Events](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.kotlin.md) ```typescript // every listener of the event await g.sys.system.emitEvent('order-placed', { order_no: 1001 }); // only these listeners await g.sys.system.emitEvent('order-placed', { order_no: 1001 }, ['send-invoice']); // in a listener const data = g.req.eventData; ``` Over HTTP : `POST …/system-api//emit-event` with `{ name, eventData, executeListeners }`. Or by itself after an API : **Automatically trigger on API hit**. The emitter gets `outputArr`, the answer of each listener. A loop of events is stopped. ### Schedulers [Schedulers](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md) | | | |---|---| | Intervals | seconds, minutes, hours, days, working days, months, a year, or cron. Each one has its time zone. | | Runs on | one server, which hands over when it stops ; or every server, once per server or once per runner | | Timeout | 10 minutes by default | | `g.req.scheduler` | `worker` of `workers`, `firedAt`, `serverIndex` of `servers`, `runnerIndex` of `runners` | | Run now | API Testing : pick the scheduler, send | | Stop | the **Active** switch, on every server at once | ### Cron [Cron](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md#custom-cron-expressions) ```text sec min hour day mon week 0 0 22 * * 1-5 22:00 Mon-Fri 0 15 14 1 * * 14:15 on the 1st */30 * * * * * every 30 seconds 0 0 */6 * * * every 6 hours ``` | | | |---|---| | sec, min | `0-59` | | hour | `0-23` | | day | day of the month, `1-31` | | mon | `1-12` or `jan-dec` | | week | day of the week, `0-7` or `sun-sat` : 0 and 7 are Sunday | A field takes `*`, `5`, `1-5`, `1,15` or `*/10`. Five fields : no seconds. Presets : `@yearly` `@monthly` `@weekly` `@daily` `@hourly` `@minutely` `@secondly` `@weekdays` `@weekends`. ### WebSockets [WebSocket events](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.kotlin.md) ```js const ws = new WebSocket('wss://host/' + '?x-am-authorization=' + token); // once CONNECTED came : ws.send(JSON.stringify({ objType: 'REGISTER', onEvents: [{ eventType: 'INSTANCES', apiName: 'SCHEMA_GET_ALL', instance: 'mongodb', database: 'shop', collection: 'orders', getEventData: true, }], })); ``` | Message | | |---|---| | `CONNECTED` | the first one : send `REGISTER` | | `REGISTER` | `validOnEvents` with their `eventId`, `invalidOnEvents` with why | | `NOTIFICATION` | `{ eventId, eventData }` after each successful call | | `UNREGISTER` | send `{ objType: 'UNREGISTER', onEvents: [eventId] }` | | `TOKEN_VALIDATION` | a token is wrong or ran out : then close `1000` | ### Subscriptions [WebSocket events](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.kotlin.md#2-subscribe) | Key | | |---|---| | `eventType` | `INSTANCES` `CUSTOM_APIS` `SYSTEM_APIS` `CUSTOM_WS_EVENTS` | | `apiName` | an API id, a custom API, a system API id, or an event name | | `condition` | `{ conditionType: 'RESPONSE', criteria: { recipientId: 42 } }` | | `select` | `{ order_no: 1 }` : the fields to send | | `getEventData` | `false` : only the fact that it happened | | | | |---|---| | Emit | `g.sys.system.emitEventWS(name, data)`, or `/emit-event-ws` | | Close `1000` | the tokens no longer let it in | | Close `1001` | the server stops : connect and `REGISTER` again | | Who | groups grant the events ; subscriptions end with the connection | ### Locks [Locks](https://docs.apimaker.dev/v4/docs/features/locks.kotlin.md) ```typescript const lock = await g.sys.system.getLock( { name: `invoice-${id}`, ttlSeconds: 30 }); if (!lock.acquired) return { sent: false }; try { // the work, one run at a time } finally { await g.sys.system.releaseLock(lock); } ``` Options : `ttlSeconds` (60), `waitSeconds`, `tries`, `retryDelayMs`, `maxHolders`, `throwIfNotAcquired`. On every server, in every language. Code only : no HTTP API. ### Cache [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.md) | | | |---|---| | Turn on | `enableCaching: true` in the settings of a table, a database, a custom or a system API | | Kept | until a write through API Maker, at most 7200 s | | Reset | `x-am-cache-control: reset_cache` · the reset system APIs · `g.sys.cache.resetCacheDB` | | Your keys | `g.sys.cache.setKey` `getKey` `removeKey` | | See it | `x-am-data-source: cache` · the Redis Dashboard | Each caller has its own cached answers : the tokens are part of the key. ## Security ### Sign-in providers [Sign-in providers](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.md) | Who | Header | In `g.req.auth` | |---|---|---| | The app | `x-am-authorization` | `authAMUser` | | Your users | `x-am-user-authorization` | `authAMDB` | | Google | `x-google-authorization` | `authGoogle` | | Azure AD | `x-azure-authorization` | `authAzure` | | AWS Cognito | `x-aws-authorization` | `authAWS` | | Your own | `x-custom-authorization` | `authCustom` | Your code reads who calls in `g.req.auth`. Require a provider with `authProviders: ['users_tg']` in the settings of an API, a table or a database, or in `common.authProviders` of the secret. ### Your users table [Auth of a database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.md) ```typescript let dbTokenGenerator: T.IAuthTokenAMDB = { name: 'users_tg', instance: 'mongodb', database: 'shop', collection: 'users', // table: in SQL usernameColumn: 'email', passwordColumn: 'password', groupsColumn: 'groups', // 'a,b' or '*' expiresInSeconds: 259200, }; module.exports = dbTokenGenerator; ``` `Security → Auth Providers`. A person signs in with `{ "name": "users_tg", "u", "p" }` on `/token`. ### The secret [Secrets](https://docs.apimaker.dev/v4/docs/secrets/secrets.kotlin.md) | | | |---|---| | `hashingAlgorithm` `nonce` | `hashing: true`, hash data | | `encryptionAlgorithm` `secret` | `encryption: true` (AES) | | `encryptionAlgorithmFETransfer` `secretFETransfer` | encrypt and decrypt data, encrypted payloads : the key of your apps | | `feTransferDataValidityInSeconds` | how old an encrypted payload may be | | `apiUserPasswords` | passwords of API users, by path | | `connectionString` | the strings the instance form offers | | `authProviders` | the providers every API needs | | `multiTenant` | the tenants tables | The keys of `common`. From code : `g.sys.system.getSecret(['common.secret'])`. A secret never goes to Git. ### Before you go live [Checklist](https://docs.apimaker.dev/v4/docs/security/go-live.md) - [ ] Change the passwords of `root@root.com` and `admin@admin.com`. Turn on two-factor sign-in. - [ ] Replace the API user `default` and the group `Default` : it grants everything. - [ ] A row scoping pre hook on each table people reach. - [ ] Run the security report : fix every finding, aim for grade A. - [ ] Review the `IS_PUBLIC` APIs and the allowed origins. - [ ] HTTPS only. Databases on a private network. Backups. Your licence key. ## Run it ### Ports [Install API Maker](https://docs.apimaker.dev/v4/docs/getting-started/local-run.md) | | | |---|---| | APIs and WebSockets | `38246` (`48246` without Launchpad) | | Admin panel, local | `4626` : `http://127.0.0.1:4626` | | MongoDB of API Maker | `38248` | | Redis of API Maker | `7479` | | Your databases, local | MongoDB `27018` · PostgreSQL `5433` · MySQL `3307` · MariaDB `3308` · Percona `3309` · TiDB `4001` · SQL Server `14330` · Oracle `1522` | Launchpad picks free ports for a new project : these are the defaults. ### Defaults [Configuration](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.md) | | | |---|---| | Time of the code | 13000 ms | | Time of a scheduler | 10 minutes | | A token | 259200 s (3 days) ; refresh 900 s more | | A cached answer | 7200 s, up to 1,000,000 characters | | Compression | answers from 51200 bytes | | A body · a form with files | 16 MiB · 512 MiB | | Uploaded files kept | 1800 s | | A lock | `ttlSeconds` 60 | ### Settings of a server [Configuration](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.md) | | | |---|---| | The page | **Settings → Deployment Settings**, root user : every server at once | | `.env` of a server | wins over the page : `am__port=48246` | | Command line | `--am.port=48246` ; `--envPath=` | | Required | `mongo_db_connection` `passCommunication` `passJWT` `passDBEncryptDecrypt` `redisInternal.nodes` | | Public address | `BE_HOST_PORT=https://api.example.com` | | Log | `AM_LOG=debug`, or `info,am_sandbox=debug` | ### Connection strings [Connection strings](https://docs.apimaker.dev/v4/docs/Database-connection-string/mongodb-connection-strings.md) | | | |---|---| | MongoDB | `mongodb://user:pass@host:27017/?authSource=admin` · `mongodb+srv://user:pass@cluster/` | | MySQL, Percona, TiDB | `mysql://user:pass@host:3306/db` | | MariaDB | `mariadb://user:pass@host:3306/db` | | PostgreSQL | `postgresql://user:pass@host:5432/db?sslmode=require` | | SQL Server | `Server=host,1433;Database=db;User Id=u;Password=p;TrustServerCertificate=True;` | | Oracle | `host:1521/service`, with a username key and a password key | The string stays in the secret : the instance names its key. Write the password as it is. ### The Git repository [Git integration](https://docs.apimaker.dev/v4/docs/Git/git.md) ```text AGENTS.md package.json tsconfig.json src/ custom_apis/orders/ orders.yaml the item orders.ts its code : .py .go … orders.config.ts its settings pre-hooks/check_stock.ts schedulers/ events/ schemas/ collection_hooks/ utility_classes/ test_cases/ admin_user/data.yaml ``` **Commit** on the Git page. **Pull** on another server : it also runs the new migration scripts. Secrets never go to Git. ### Local client [Developer tools](https://docs.apimaker.dev/v4/docs/features/developer-tools.md) | | | |---|---| | `npm run start` | keeps the folder in sync, both ways | | `npm run test` | runs the test cases ; `--no-sync`, `--push` | | `local-client.config.js` | the sync token : not in Git | | A CI job | `AM_WEB_SOCKET_URL` `AM_SYNC_TOKEN` `AM_ADMIN_USER_PATH` | Up to 50 local clients on one account. Nothing is committed for you. ### Debugging [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md) 1. Menu of your user → **Debug Settings** → **Debugging** on. 2. API Testing opens on **Debug**. Click left of a line, or F9 : a breakpoint. 3. Right click the margin : a condition, a hit count, a logpoint. 4. Another client sends `x-am-run-in-debugger: true`. Every language, in the sandboxes and on the native process. ### SSR websites [Reference](https://docs.apimaker.dev/v4/docs/ssr/api-reference.md) | | | |---|---| | `pages/**/*.tsx` | a page : `default`, `loader`, `action`, `config`, `head` | | `pages/**/*.ts` | an endpoint : `GET` `POST` `PUT` `PATCH` `DELETE` `ALL` | | `_layout.tsx` `_middleware.ts` | the layout, the middleware | | `_404.tsx` `_error.tsx` | the pages of errors | | `site.config.ts` | `defineSite({ … })` | In a page, `ctx` has `db`, `api`, `cache`, `auth`, `cookies`, `fetch`. The module `am-ssr` has `redirect`, `notFound`, `json`, `formatDate`, `paginate`… ## Admin panel ### Where things are [The admin panel](https://docs.apimaker.dev/v4/docs/getting-started/admin-panel.md) | | | |---|---| | APIs & Logic | Instance API (DB API) · Custom API · SSR Websites · IDE · System API · Events · Schedulers · WebSockets · Initializers · Utility Classes · API Testing · Test Cases · i18N Management · Generated Interfaces · Swagger Docs | | Security | APIs Security Report & Actions · API Group Permissions · API User Permissions · Auth Providers · Secret Management · Vulnerabilities · Encrypt Strings · Dev Account | | Database | ER Diagram · Index Maker · Database Migration · Auto Increments | | Operations | Analytics · Log Explorer · Servers · Redis Dashboard · Git Integration · Automatic Backup · Health Check AI Reporting · AI Integration · Log Profile · Native Process Logs · Sandbox Settings · Upgrade/Downgrade Engine · Licenses | | Settings | Web Server · Deployment Settings (root user) | | Your user | User Info (Git URL) · My Security · Debug Settings · Local Client Sync Token | | Any page | Cmd+K or Ctrl+K : its name, then Enter · ? explains the controls of a page | ### Keys of the IDE and the editors [IDE](https://docs.apimaker.dev/v4/docs/features/ide.md) | | Mac | Windows, Linux | |---|---|---| | Go to file | Cmd+P | Ctrl+P | | Search in the files | Cmd+Shift+F | Ctrl+Shift+F | | Explorer | Cmd+Shift+E | Ctrl+Shift+E | | Side · panel | Cmd+B · Cmd+J | Ctrl+B · Ctrl+J | | Back, forward | Ctrl+Option+Arrow-left Ctrl+Option+Arrow-right | Alt+Arrow-left Alt+Arrow-right | | | Mac | Windows, Linux | |---|---|---| | Close the tab | Option+W | Alt+W | | Git blame | Option+A | Alt+A | | Save, save all | Cmd+S Cmd+Option+S | Ctrl+S Ctrl+Alt+S | | Breakpoint | F9 | F9 | | Send, run the code | Cmd+E | Ctrl+E | --- # API Maker Docs for AI Assistants > How to give ChatGPT, Claude, Cursor, Copilot or any agent the API Maker documentation - llms.txt and llms-full.txt, the same files and the Markdown of every page with the code of one language only, the files of every kind of item, the AGENTS.md API Maker writes into your repository, and the security report an assistant can act on. Source: https://docs.apimaker.dev/v4/docs/ai/llms.html API Maker is built for the AI era, and so is this documentation. Every page is available as plain Markdown, with the code of the language your assistant writes, an index lists them all for agents, and an assistant working in your project folder finds the code and the security report of your account as files. - Video: [Build Your Backend with AI Assistants That Follow One Standard](https://apimaker.dev/videos/ai-assistants) (4 min) ## Files an assistant can read | What | URL | Use it for | |---|---|---| | Index of the documentation | [`https://docs.apimaker.dev/llms.txt`](https://docs.apimaker.dev/llms.txt) | One page per line with its title, description and Markdown URL, grouped like the navigation. Give it to an agent so it fetches only the pages it needs. | | The whole documentation | [`https://docs.apimaker.dev/llms-full.txt`](https://docs.apimaker.dev/llms-full.txt) | Every page in one text file, in reading order, with the code in TypeScript only and without the raw output of the benchmarks. Upload it to a tool which searches its files (a project knowledge base), or give an agent `llms.txt` and let it fetch the pages it needs. | | One part of it | [`llms-getting-started.txt`](https://docs.apimaker.dev/llms-getting-started.txt), [`llms-security.txt`](https://docs.apimaker.dev/llms-security.txt), [`llms-build.txt`](https://docs.apimaker.dev/llms-build.txt), [`llms-operate.txt`](https://docs.apimaker.dev/llms-operate.txt), [`llms-code-examples.txt`](https://docs.apimaker.dev/llms-code-examples.txt), [`llms-benchmarks.txt`](https://docs.apimaker.dev/llms-benchmarks.txt), [`llms-release-notes.txt`](https://docs.apimaker.dev/llms-release-notes.txt) | The pages of one tab of the navigation, the same way : a smaller file for an assistant which works on one part. | | Any page as Markdown | the page URL with `.md` instead of `.html`, for example [`/v4/docs/schema/schema.md`](https://docs.apimaker.dev/v4/docs/schema/schema.md) | Clean text without navigation, with the snippets resolved and the diagrams replaced by their caption. | | The same, with the code of one language | see [below](https://docs.apimaker.dev/v4/docs/ai/llms.md#one-language) | What an assistant which writes Python, Go or Java reads. | | The website for AI | [`https://apimaker.dev/llms.txt`](https://apimaker.dev/llms.txt) and [`llms-full.txt`](https://apimaker.dev/llms-full.txt) | The feature pages and the deployment architectures. | Every page also carries `` pointing at its Markdown twin, and JSON-LD structured data, so crawlers and agents find the right version on their own. ## The code of one language The pages show the code in the 8 languages of API Maker, a tab each. An assistant writes one language : given the code of all eight, it can mix them. So every file above also exists with the code of one language only. | What | URL | |---|---| | A page, with the code of one language | the page URL with `..md` instead of `.html` : [`/v4/docs/apis-all/hooks/preHook-api.python.md`](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.python.md). Its links lead to the pages in the same language. A page whose text is the same in every language (no tabs of code) has only its `.md`. | | The index of one language | [`/ai/python/llms.txt`](https://docs.apimaker.dev/ai/python/llms.txt) : every page, each linked to its Markdown in that language. | | The whole documentation in one language | [`/ai/python/llms-full.txt`](https://docs.apimaker.dev/ai/python/llms-full.txt), and one file per part : [`/ai/python/llms-build.txt`](https://docs.apimaker.dev/ai/python/llms-build.txt), [`/ai/python/llms-code-examples.txt`](https://docs.apimaker.dev/ai/python/llms-code-examples.txt)… | The languages are `typescript` (TypeScript and JavaScript), `python`, `php`, `csharp`, `go`, `rust`, `java` and `kotlin` : [`/ai/go/llms.txt`](https://docs.apimaker.dev/ai/go/llms.txt), [`/ai/kotlin/llms-full.txt`](https://docs.apimaker.dev/ai/kotlin/llms-full.txt). The Markdown of every page with tabs names the same page in the other languages at its top, and [`llms.txt`](https://docs.apimaker.dev/llms.txt) names the files of every language. The settings of an item (the basic info of a custom API, a schema, the settings of a table) are TypeScript in every language : those blocks stay in every file. ## The files of every item To add a custom API, a hook, an event, a scheduler or a test case, an agent writes the files of the item into the folder of the account : its YAML, its code and its settings. [Items as files](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md) gives every kind, with its folder, the keys of its YAML and its code in each language. ## On every page The bar at the top of a page offers **Copy as Markdown** (the Markdown twin, ready to paste in a chat), **View Markdown**, and **Open in ChatGPT** or **Open in Claude**, which start a chat with the page already loaded. On a page with tabs of code, the bar takes the language picked in the tabs (it says **This page in Python**) : what it copies, shows and sends has the code of that language only. ## Prompts which work well **Ask about a feature** ```text Read https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md and https://docs.apimaker.dev/v4/docs/schema/schema.md, then write the schema of my orders table so that a customer is populated from the customers table in another database. ``` **Write a custom API** ```text Using https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.python.md and https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.python.md, write a custom API for API Maker in Python which returns the ten latest orders of the person in g.req.auth.authAMDB. ``` **Add an item to the repository of the account** ```text Read https://docs.apimaker.dev/v4/docs/ai/items-as-files.go.md, then add to src/ a scheduler in Go which removes the carts not changed for a week, every night at 02:30 Asia/Kolkata : its YAML and its code. ``` **Give an agent the documentation of its language** ```text The backend platform we use is API Maker, and we write its code in Kotlin. Its documentation, with the code in Kotlin, is listed at https://docs.apimaker.dev/ai/kotlin/llms.txt : fetch the pages you need from it. ``` **Write a page of an SSR website** ```text Read https://docs.apimaker.dev/v4/docs/ssr/ai-guide.md and https://docs.apimaker.dev/v4/docs/ssr/data.md, then write pages/account/orders.tsx for my SSR website : the orders of the signed-in customer, newest first, 20 per page, and the rule of site.config.ts which keeps each customer to its own orders. ``` **Give an agent the whole documentation** ```text The documentation of the backend platform we use is at https://docs.apimaker.dev/llms.txt. Fetch the pages you need from it before answering questions about API Maker. ``` ## An assistant in your project folder With the [local client](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#your-own-editor) (or the Local Client page of [API Maker Launchpad](https://docs.apimaker.dev/v4/docs/getting-started/local-run.md)), every item of the account is a file in a Git repository : `src/custom_apis/…`, `src/utility_classes/…`, `src/schemas/…`, hooks, events, schedulers, migrations, test cases, settings and the SSR websites (`src/ssr/…`). An agent such as Claude Code, Cursor or Copilot can read and change them ; the local client syncs what it saves to API Maker. - **The repository explains itself** : API Maker writes an [`AGENTS.md`](https://docs.apimaker.dev/v4/docs/ai/llms.md#agents-md) at its root, the guide an agent reads before it starts. - **A new item is a new folder** : [Items as files](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md) gives the files of every kind, so an agent writes a custom API, a hook or a scheduler which saves at once. - **Types come with the code** : `import * as T from 'types'` gives every interface of API Maker and `import * as db from 'db-interfaces'` the interfaces generated from your schemas, so an assistant writes typed code. - **Tests run from the terminal** : `npm run test` runs the [test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md) of the account with their mocks and writes the coverage to `coverage/lcov.info`. - **The security report is in the repository** : `src/api_security_report/report.md` says where a person could read rows which are not theirs and how to fix it. It is kept fresh while the local client is connected, so an assistant can fix hooks, groups and settings and see the result on the next scan. See [APIs Security Report & Actions](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.md#in-the-repository). - **The sample account is a tutorial** : a new account comes with a sample shop (schemas, custom APIs, hooks, an event, a migration, a utility class) written to be read. Point the assistant at `src/custom_apis/` before asking for something similar. ## AGENTS.md in your repository Agents such as Codex, Cursor, Copilot and Claude Code read a file called `AGENTS.md` at the root of a folder before they work in it. API Maker writes one into the [Git repository](https://docs.apimaker.dev/v4/docs/Git/git.md) of the account on every commit, so an agent knows API Maker before its first question : - **Where things are** : the folder of every kind of item, the files of an item (its YAML, its code, its settings in TypeScript), how to add one, and the files API Maker writes, which an agent never edits. - **How code is written** : `main` and `g` in all eight languages, the calls of `g.sys`, hooks, utility classes, test cases, and where the exact types are (`am-types/types.ts`, `src/types/db-interfaces.d.ts`). - **The rules of security** : the two gates, the row scoping pre-hook, secrets, what code must never do. - **The security report** : how to read `src/api_security_report/report.md`, fix a finding in the file it names and see it go, without ever skipping a finding : a skip is the decision of your team. - **How to check a change** : the local client, `npm run test`, the type checks of each language, and the pages of these docs as Markdown, with the code of the language it writes ([`/ai//llms.txt`](https://docs.apimaker.dev/v4/docs/ai/llms.md#one-language)). It is about 14 KB, short enough to be read at the start of every session, and it goes with the version of API Maker : an update of API Maker brings the guide of that version with the next commit. The guide is a block between two lines, `` and ``. Every commit writes the block again; what you write before or after it is yours and stays as it is. A repository which has an `AGENTS.md` of its own gets the block after its text. **AGENTS.md** ```markdown # Guide for AI agents … # Our team - Every new table gets a row scoping pre-hook before its first API user. - Ask Diya before a migration script which deletes rows. ``` - The file comes to your folder with `git pull`, with the rest of the commit. The local client does not write it : git refuses a pull which would replace a file it does not track. - The Git page lists it under **AGENTS.md (guide for AI agents)** the first time, and after an update of API Maker which changed the guide. It can not be reverted from the page : the next commit writes it again. - Claude Code reads `CLAUDE.md` : a `CLAUDE.md` with the line `@AGENTS.md` makes it read the guide too. ## Swagger for the APIs of your account Every API user can have its own Swagger (OpenAPI) document with the APIs it may call, with a URL to share : enable it on the [API user](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#swagger-docs-per-api-user). An assistant which generates a client for your APIs reads it directly. > **Tell us what is missing** > > If an assistant gives a wrong answer about API Maker, the page it read is probably unclear. Write to [contact@apimaker.dev](mailto:contact@apimaker.dev) with the question and the page ; it helps every reader. --- # Items as Files - What to Write for Each Kind of Item > The files API Maker reads for every kind of item, in the Git repository and in the folder of the local client - the folder, the YAML, the settings and the code of custom APIs, hooks, events, WebSocket events, schedulers, migrations, process initializers, utility classes, test cases, schemas, settings, auth providers, groups, API users and instances - so an AI agent writes a new item which saves at once, in its language. Source: https://docs.apimaker.dev/v4/docs/ai/items-as-files.html Every item of an account is a folder of files in its [Git repository](https://docs.apimaker.dev/v4/docs/Git/git.md) and in the folder of the [local client](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#your-own-editor). An AI agent which adds a custom API, a hook or a scheduler writes these files : this page says which files each kind has, what is in each one, and what makes an item new. Pick your language in the tabs : the files of the code follow it. > **In short** > > - `src///` holds `.yaml` (the item, without its code), `.` (its code : the extension is its language) and, for a few kinds, `.config.ts` (its settings, TypeScript in every language). > - A new item has no `_id` and no `guid` : API Maker gives them. Code next to the YAML is read when the YAML lists `versions` with one version `"-1"`. > - Run the local client (`npm run start`) : a saved folder is checked and saved the way the admin panel saves it, and `✗ Not applied on server : …` says what is wrong. ## The whole tree A repository with one item of each kind, its code in the language of the tab : **A repository, the code in Kotlin** ```text AGENTS.md the guide for AI agents, written by API Maker package.json tsconfig.json am-types/types.ts localClient.js written by API Maker settings.gradle.kts am/kotlin/build.gradle.kts am/jvm/am/ the project of the language, written by API Maker src/ custom_apis/shippingCost/ shippingCost.yaml name, path, versions and their hooks (no code) shippingCost.config.ts path, method, access, schemas : TypeScript in every language shippingCost.kt the code pre-hooks/check_weight/check_weight.kt a hook of the API, named in versions[].preHooks collection_hooks/Row_scope-orders/ one folder per hook Row_scope-orders.yaml its table, pre or post, its order Row_scope-orders.kt events/Order_placed/ Order_placed.yaml its listeners, its triggers listeners/Notify_the_shop/Notify_the_shop.kt web_socket_events/defaultorders-updated/ the event default/orders-updated defaultorders-updated.yaml defaultorders-updated.kt who may subscribe schedulers/Clean_old_carts/ Clean_old_carts.yaml intervals, isActive Clean_old_carts.kt database_migrations/Add_description/ Add_description.yaml label, order, active Add_description.kt process_initializers/Load_rates/ Load_rates.yaml order Load_rates.kt utility_classes/utils---Money/ the class Money of the folder utils/ Money.yaml Money.kt test_cases/Shipping_cost/ Shipping_cost.yaml what it tests, its mocks Shipping_cost.kt the tests schemas// a table : its YAML and .config.ts instance_api_settings/orders/ settings of a table : orders.yaml, orders.config.ts system_api_settings/HASH_DATA/ settings of a system API : HASH_DATA.yaml, HASH_DATA.config.ts auth_providers/default_db_login/ default_db_login.yaml default_db_login.ts what the provider is : TypeScript in every language default_db_login.fg/default_db_login.fg.kt its fields generator groups/Shop_app/Shop_app.yaml api_users/shop_app/shop_app.yaml instances/mongodb/mongodb.yaml i18ns/hindi/hindi.yaml hindi.ts admin_user/data.yaml sandboxes : runtimes, versions, packages ssr// an SSR website, in TypeScript types/db-interfaces.d.ts api_security_report/ auto_increments/ written by API Maker ``` ## The rules of every item ### Folders and file names | Rule | Example | |---|---| | One folder per kind in `src`, the kind in small letters with `_` | `src/custom_apis`, `src/web_socket_events` | | One folder per item : its name, where what is no letter, digit, `.`, `-` or `_` is written `_` | the scheduler `Daily report` is `src/schedulers/Daily_report/` | | ` - ` and ` -- ` between the parts of a name lose their spaces | the hook `Row scope - orders` is `Row_scope-orders/` | | The files of an item are named as its folder | `Daily_report/Daily_report.yaml`, `Daily_report/Daily_report.py` | | The name itself is `name:` in the YAML : nothing is read back from the folder | `name: Daily report` | - A utility class is its folder with `-` for `/`, then `--`, then its name : the class `Money` of the folder `utils/` is `src/utility_classes/utils---Money/` (of `cloud/db/` : `cloud-db---Money/`), its YAML `Money.yaml`, its code `Money.`. - A schema is the folder of its guid when API Maker writes it. A new one can have any folder (the name of its table) : the next commit writes it under its guid. - The settings of a system API are the folder of the API : `src/system_api_settings/GET_TOKEN/`. A WebSocket event is its name without `/` and `@` : `default/orders-updated` is `defaultorders-updated/`. - Two hooks of one name : the second one gets its table, then a number (`Check_access`, `Check_access--orders`). See [Hooks in Git](https://docs.apimaker.dev/v4/docs/Git/git.md#hooks). ### The YAML - The item as the admin panel saves it, without its code and without the keys of its settings file. Write the keys this page shows for its kind ; API Maker writes the others back with their defaults. - **A new item** : no `_id` and no `guid`. A guid copied from another item replaces that item : never copy a folder with its guid. - The language of the code is the extension of its file : `orders.py` next to `orders.yaml` is Python. `codeLanguage: python` in the YAML says the same, and wins over the extension. ### The code - One function `main` in each file, which gets `g` : what it returns is the answer. The code blocks of this page show it in each language ; the [global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) has every call. - An API and all its hooks are one language. In Go, Java and Kotlin, a piece of code which shares a folder with other code (the hooks of a custom API, the functions of an auth provider) is in a folder named as its file : `pre-hooks/check_weight/check_weight.go`. - A file of Go never ends as Go reads names : `orders_test` is written `orders_test_.go`. ### Versions - `versions` in the YAML is the list of the versions of the code. One version `"-1"` (named `Original`) : the code is `.`, next to the YAML. - Several versions : `versions//.`, its hooks in `versions//pre-hooks/` and `post-hooks/`, and `isActive: true` on the version which runs. ### The settings - `.config.ts` for a custom API, a schema and the settings of a database, a table, an API or a system API ; `.ts` for an auth provider and for texts (i18n). They are **TypeScript in every language** : API Maker runs them itself when it saves. Each exports one object : `module.exports = customApi;`. - The settings file wins over the YAML for the keys it has : the `name`, `path`, `requestMethod`, `apiAccessType`… of a custom API, the columns of an auth provider. Keep `name` the same in both : it names the folder. ### Files API Maker writes : never edit them `AGENTS.md` (the block of API Maker), `am-types/types.ts`, `src/types/db-interfaces.d.ts`, `localClient.js`, `package.json`, `tsconfig.json`, `pyproject.toml`, `composer.json`, `go.mod`, `Cargo.toml`, `ApiMaker.csproj`, `settings.gradle.kts`, `am/`, `src/api_security_report/`, `src/auto_increments/`. A package goes into `src/admin_user/data.yaml` : see [Packages](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#packages). ### Check a change - With the local client : `npm run start` sends each saved folder, and the server answers `✓ Saved on server` or `✗ Not applied on server : `. Deleting the folder of an item deletes the item. - `npm run test` runs the [test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md) of the account with the code of the folder. - The compiler of the language, offline : ```bash gradle check ``` ## Every kind at a glance | Kind | Folder in `src` | Files | Code | |---|---|---|---| | [Custom API](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#custom-api) | `custom_apis/` | YAML, `.config.ts`, code, `.docs.json`, `pre-hooks/`, `post-hooks/`, `versions/` | `main`, the answer | | [Hook of an instance, a database, a table, an API, a system API](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#hooks) | `instance_hooks/`, `database_hooks/`, `collection_hooks/`, `instance_api_hooks/`, `system_api_hooks/` | YAML, code : one folder per hook | `main`, before or after the APIs | | [Event](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#event) | `events/` | YAML, `listeners//.` | one `main` per listener | | [WebSocket event](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#web-socket-event) | `web_socket_events/` | YAML, code | who may subscribe | | [Scheduler](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#scheduler) | `schedulers/` | YAML, code | the job | | [Database migration](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#database-migration) | `database_migrations/` | YAML, code | runs once per environment, after a pull | | [Process initializer](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#process-initializer) | `process_initializers/` | YAML, code | runs once per runner, when it starts | | [Utility class](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#utility-class) | `utility_classes/` | YAML, code | shared by the code of its language | | [Test case](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#test-case) | `test_cases/` | YAML, code | the tests | | [Schema](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#schema) | `schemas/` | YAML, `.config.ts` | none : TypeScript settings | | [Settings of a database, a table, an API](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#settings) | `instance_api_settings/` | YAML, `.config.ts` | none | | [Settings of a system API](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#system-api-settings) | `system_api_settings/` | YAML, `.config.ts` | none | | [Auth provider](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#auth-provider) | `auth_providers/` | YAML, `.ts`, `.fg.`, `.tg.`, `.tv.` | the functions of the provider | | [Group, API user, instance](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#access) | `groups/`, `api_users/`, `instances/` | YAML | none | | [Texts, log profiles, saved requests](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#other-kinds) | `i18ns/`, `log_profiles/`, `api_testing_states/` | YAML, and `.ts` for texts | none | | [Packages and sandboxes](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#packages) | `admin_user/` | `data.yaml`, `sandbox//Dockerfile` | none | | [SSR website](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#ssr-website) | `ssr//` | a project of TypeScript | pages | ## Custom API A function behind a path and a method : see [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md). The custom API `shippingCost` gives the cost of a parcel from the rates table ; the [test case](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#test-case) below tests it. **src/custom_apis/shippingCost/shippingCost.yaml** ```yaml name: shippingCost # the same name as in shippingCost.config.ts : it names the folder and the files path: /shipping-cost # as in the settings file, which wins versions: - name: Original version: "-1" # "-1" : the code is the file next to this one, shippingCost. isActive: true preHooks: [] # the hooks of the API, see below postHooks: [] ``` **src/custom_apis/shippingCost/shippingCost.config.ts** ```typescript import * as T from 'types'; // The settings of the API : TypeScript, whatever the language of its code. const customApi: T.ICustomApiSettingsTypes = { name: 'shippingCost', path: '/shipping-cost', // POST /api/custom-api//shipping-cost requestMethod: T.ERequestMethod.POST, apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, // IS_PUBLIC : no token ; NO_ACCESS : from code only errorList: ['Too heavy to ship.'], // the messages it throws, for i18n and its docs reqBodySchema: { // checked before the code runs weight: { __type: T.EType.number, validations: { required: true, min: 0 } }, }, }; module.exports = customApi; ``` **src/custom_apis/shippingCost/shippingCost.kt** ```kotlin import am.AmErrors import am.G import am.GetAll fun main(g: G): Any? { val weight = g.req.body.getDouble("weight") if (weight > 30) throw AmErrors("Too heavy to ship.", 422) val rates = g.sys.db.getAll(GetAll("mysql_8", "shop", "rates")) return mapOf("cost" to weight * rates[0].getDouble("per_kg")) } ``` - **Its hooks** are named in the version they run with, and their code is in `pre-hooks/` and `post-hooks/` of the folder of the API : `preHooks: [{ name: check_weight, isActive: true }]` is `pre-hooks/check_weight.` (in Go, Java and Kotlin `pre-hooks/check_weight/check_weight.`). A hook of an API is in the language of the API ; `groupNames` on a hook runs it for the API users of those groups only. - **Its docs** (the Docs tab) are `shippingCost.docs.json`, which the panel writes. An API without docs has no such file. - **Its path** is unique with its method, and its `name` is unique in the account. ## Hooks The hooks of an instance, a database, a table, an API of a table and a system API are a folder each, named by the hook. See [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) and [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md). | Hook of | Folder in `src` | Where it runs : keys of its YAML | |---|---|---| | An instance | `instance_hooks/` | `instanceName` | | A database | `database_hooks/` | `instanceName`, `databaseName` | | A table | `collection_hooks/` | `instanceName`, `databaseName`, `collectionName` | | One API of a table | `instance_api_hooks/` | `instanceName`, `databaseName`, `collectionName`, `apiId` (`SCHEMA_GET_ALL`, `GEN_POST_QUERY`…) | | A system API | `system_api_hooks/` | `apiId` (`GET_TOKEN`…) | **src/collection_hooks/Row_scope-orders/Row_scope-orders.yaml** ```yaml name: Row scope - orders # the folder : Row_scope-orders instanceName: mongodb # where it runs : the table orders databaseName: shop collectionName: orders hookType: pre # pre : before the APIs of the table ; post : after them order: 1 # its place among the pre hooks of the table, from 1 isActive: true groupNames: [Shop app] # only for the API users of these groups ; leave it out for all of them ``` - A new hook needs no `_id` and no `guid` : it joins the other hooks of its place. Delete its folder and the hook goes. - The code is `Row_scope-orders.`, next to its YAML, in every language : a hook of a table is alone in its folder. **src/collection_hooks/Row_scope-orders/Row_scope-orders.kt** ```kotlin import am.G fun main(g: G): Any? { if (!g.req.isApiRequestFromUser) return null // calls made by your own code : leave them alone // g.req.query, g.req.body, g.req.params, g.req.headers : the request, changeable // g.req.auth.getObject("authAMUser"), g.req.auth.getObject("authAMDB")… : who calls // g.res.shared : values for the API and the post hooks of the same request return null } ``` - The code which keeps each person to its own rows is on [Narrow a request to the rows of the caller](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md#narrow-a-request-to-the-rows-of-the-caller), in every language. A table which people read through an application needs it before its first API user : the [security report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.md) says where it is missing. ## Event An event has listeners, each one a folder of code under `listeners/`. See [Events](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.kotlin.md). **src/events/Order_placed/Order_placed.yaml** ```yaml name: Order placed # g.sys.system.emitEvent('Order placed', data) emits it isAutomaticallyTriggerOnApiHit: false # true : emitted after each successful call of the APIs of triggerOnApis triggerOnApis: [] eventListeners: - name: Notify the shop # its code : listeners/Notify_the_shop/Notify_the_shop. versions: - name: Original version: "-1" isActive: true ``` **src/events/Order_placed/listeners/Notify_the_shop/Notify_the_shop.kt** ```kotlin import am.G import am.JsonObject import am.Save fun main(g: G): Any? { val data = JsonObject.from(g.req.eventData) // what was emitted g.sys.db.saveSingleOrMultiple( Save("mongodb", "shop", "notifications", mapOf("order_id" to data.getLong("id"), "text" to "Order ${data.getLong("id")} placed")), ) return mapOf("notified" to true) // returned to the emitter } ``` - A listener is a folder of its own in every language : `listeners//.`. - To emit it after a successful call of an API instead of from code : `isAutomaticallyTriggerOnApiHit: true`, and the API in `triggerOnApis` (`{ apiCategory: CUSTOM_APIS, customApiName: shippingCost }`, or an API of a table with `instanceName`, `databaseName`, `collectionName`, `instanceApiName`). ## WebSocket event A custom WebSocket event, emitted by `g.sys.system.emitEventWS`. Its code says who may subscribe. See [WebSocket events](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.kotlin.md#custom-websocket-events). **src/web_socket_events/defaultorders-updated/defaultorders-updated.yaml** ```yaml name: default/orders-updated # the folder is the name without its / apiCategory: WEB_SOCKET_EVENT authProviders: [default_db_login] # a client subscribes with a person token of these providers versions: - name: Original version: "-1" isActive: true ``` **src/web_socket_events/defaultorders-updated/defaultorders-updated.kt** ```kotlin import am.G // Who may subscribe : a person of the users table. fun main(g: G): Any? { if (g.req.auth.getObject("authAMDB").isEmpty()) return mapOf("canConnect" to false, "errorText" to "Please sign in.") return mapOf("canConnect" to true, "errorText" to "") } ``` ## Scheduler Code which runs on a timetable. See [Schedulers](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md). **src/schedulers/Clean_old_carts/Clean_old_carts.yaml** ```yaml name: Clean old carts isActive: true # the switch of the whole scheduler intervalArr: - timeInterval: 0 30 2 * * * # cron, seconds first : every day at 02:30 timeZone: Asia/Kolkata active: true # each interval has its own switch schedulerTimeoutInMinutes: 10 versions: - name: Original version: "-1" isActive: true ``` **src/schedulers/Clean_old_carts/Clean_old_carts.kt** ```kotlin import am.G import am.RemoveByQuery fun main(g: G): Any? { val before = System.currentTimeMillis() - 7 * 86400000L // a week ago, in milliseconds since 1970 : what updated_at holds val removed = g.sys.db.removeByQuery(RemoveByQuery("mongodb", "shop", "carts").find(mapOf("updated_at" to mapOf("\$lt" to before)))) g.logger.log("${removed.deletedRowsCount} carts removed") return mapOf("removed" to removed.deletedRowsCount) } ``` ## Database migration A script which runs once on each environment, after the pull which brings it. See [Database migration](https://docs.apimaker.dev/v4/docs/features/database-migration.kotlin.md). **src/database_migrations/Add_description/Add_description.yaml** ```yaml name: Add description label: Add the column description to order_transactions # what the list of scripts shows order: 1 # the pull runs the scripts which never ran here, from the lowest order active: true dbMigrationTimeoutInMinutes: 10 ``` **src/database_migrations/Add_description/Add_description.kt** ```kotlin import am.ExecuteQuery import am.G import am.GetTableMeta fun main(g: G): Any? { val meta = g.sys.system.getTableMeta(GetTableMeta().instance("mysql8").database("inventory").table("order_transactions")) val hasDescription = meta.any { it.name == "description" } if (!hasDescription) { g.sys.system.executeQuery( ExecuteQuery("mysql8", "ALTER TABLE `inventory`.`order_transactions` ADD COLUMN `description` varchar(255) NULL AFTER `qty`"), ) } return mapOf("hasDescription" to hasDescription) } ``` - Write a script which can run twice without harm : a script which failed half way runs again after the fix. ## Process initializer Code which runs once in each runner of the sandboxes of the account, when it starts. See [Process initializers](https://docs.apimaker.dev/v4/docs/features/process-initializers.kotlin.md). **src/process_initializers/Load_rates/Load_rates.yaml** ```yaml name: Load rates order: 1 # the initializers run one after the other, from the lowest order active: true processInitializerTimeoutInMinutes: 1 ``` **src/process_initializers/Load_rates/Load_rates.kt** ```kotlin import am.G import am.GetAll fun main(g: G): Any? { // Read once per process : every request this process serves finds them in g.shared. val rates = g.sys.db.getAll(GetAll("mysql_8", "shop", "rates")) g.shared["rates"] = rates g.logger.log(rates.size, "rates loaded") return null } ``` ## Utility class Code which the other code of its language imports. See [Utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.kotlin.md#other-languages). **src/utility_classes/utils---Money/Money.yaml** ```yaml name: Money # what the code imports ; the file is named Money.yaml, not as the folder folderPath: utils/ # its folder in the list : the folder in git is utils---Money versions: - name: Original version: "-1" # the code is Money., next to this file isActive: true ``` The class is `src/utility_classes/utils---Money/Money.kt`, compiled with the other classes of Kotlin. **The utility class Money, in the folder utils** ```kotlin object Money { fun round(amount: Double): Double = Math.round(amount * 100) / 100.0 } ``` **A custom API which uses it** ```kotlin import am.G fun main(g: G): Any? = mapOf("total" to Money.round(g.req.body.getDouble("amount") * 1.18)) ``` - A class is used by the code of its own language only. ## Test case The tests of a custom API or of a utility class, run by `npm run test` and the Test Cases page. See [Test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md). **src/test_cases/Shipping_cost/Shipping_cost.yaml** ```yaml name: Shipping cost testCaseType: CUSTOM_APIS # UTILITY_CLASS with utilityClassName for a utility class customApiName: shippingCost # the custom API it tests mocks: [] # the mocks of the Mocks tab ; the code adds its own ``` The test case is `src/test_cases/Shipping_cost/Shipping_cost.kt`. **A test case** ```kotlin import am.AmErrors 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"] } } fun testTooHeavyIsRefused(g: G) { try { g.sys.test.runCustomApi(RunCustomApi("shippingCost").body(mapOf("weight" to 40))) } catch (refused: AmErrors) { check(refused.code == 422) { "refused with ${refused.code}" } return } error("a parcel of 40 kg was accepted") } ``` - How each language names its tests : [Test cases in every language](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md#other-languages). ## Schema The columns of a table, their checks and conversions, and the relations to other tables. TypeScript in every language. See [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md). **src/schemas/orders/orders.yaml** ```yaml instanceName: mongodb # the table the schema is for databaseName: shop collectionName: orders active: true ``` **src/schemas/orders/orders.config.ts** ```typescript import { EType, ISchemaType, ISchemaProperty } from 'types'; const schema: ISchemaType = { _id: { __type: EType.objectId, isPrimaryKey: true, isAutoGenerateByAM: { valueGeneratorType: 'ObjectID' }, }, customer_id: { // a relation : orders.customer_id is customers.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 } }, }; module.exports = { schema }; ``` - The YAML is named as the folder. API Maker writes a schema in the folder of its guid (`src/schemas/01M6NW232XSTGXXD4222CXZDM7/`) : a new one goes in any folder, and the next commit writes it under its guid. - `src/types/db-interfaces.d.ts` gets the interface of the table once the schema is saved (`db.mongodb.shop.IOrders`). ## Settings of a database, a table, an API Who may call the APIs of a table, their caching and their auth providers. See [Collection settings](https://docs.apimaker.dev/v4/docs/settings/collectionSettings.md). **src/instance_api_settings/orders/orders.yaml** ```yaml name: orders # names the folder ; the guid of the settings when it has none instanceName: mongodb databaseName: shop collectionName: orders apiId: __COLLECTION_SETTINGS__ # the table ; SCHEMA_GET_ALL… : one API of it ; # __DATABASE_SETTINGS__ here and in collectionName : the whole database ``` **src/instance_api_settings/orders/orders.config.ts** ```typescript import * as T from 'types'; let instanceColSetting: T.IInstanceApiSettingsTypes = { enableCaching: true, // the reads are cached until a write of the table acceptOnlyEncryptedData: false, apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, authProviders: ['default_db_login'], // every API of the table needs a person token of this provider }; module.exports = instanceColSetting; ``` - A key left out is taken from the level above : API settings, then the table, then the database, then the secret. ## Settings of a system API **src/system_api_settings/HASH_DATA/HASH_DATA.yaml** ```yaml apiId: HASH_DATA # the folder and the files are named by the API ``` **src/system_api_settings/HASH_DATA/HASH_DATA.config.ts** ```typescript import * as T from 'types'; let systemApi: T.ISystemApiSettingsTypes = { apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, // NO_ACCESS without settings : only g.sys reaches it }; module.exports = systemApi; ``` - The ids : [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md). ## Auth provider Who the person behind a request is : a row of your users table, Google, Azure AD, AWS Cognito or your own code. See [Auth of a database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.md). **src/auth_providers/default_db_login/default_db_login.yaml** ```yaml name: default_db_login generatorType: DB_TOKEN_GENERATOR # CUSTOM_TOKEN_GENERATOR, GOOGLE, AZURE, AWS : the other kinds ``` **src/auth_providers/default_db_login/default_db_login.ts** ```typescript import * as T from 'types'; // What the provider is : TypeScript in every language. It wins over the YAML. const provider: T.IAuthTokenAMDB = { name: 'default_db_login', instance: 'mongodb', database: 'shop', collection: 'customers', // table: '…' for SQL usernameColumn: 'email', passwordColumn: 'password', // a column the schema hashes expiresInSeconds: 259200, }; module.exports = provider; ``` - Its functions are in the language of the provider, next to it : `default_db_login.fg.` (the fields it adds to the token, may be empty), and for a provider of your own code `.tg.` (it makes the token) and `.tv.` (it checks one). In Go, Java and Kotlin each one is in a folder named as its file : `my_tokens.tg/my_tokens.tg.go`. Their code : [A provider of your own](https://docs.apimaker.dev/v4/examples/req/auth/authCustom.kotlin.md). ## Group, API user, instance **src/groups/Shop_app/Shop_app.yaml** ```yaml name: Shop app apisRegex: # true : every item of a kind ; false : only the ones named below allInstances: false allDatabases: false allCollections: false allInstanceApis: false allFields: true allCustomApis: false allSystemApis: false allWebSocketEvents: false queryParams: # the grants, by name instanceNames: [mongodb] databaseNames: [shop] collectionNames: [products, orders] supportedApiNames: [SCHEMA_GET_ALL, SCHEMA_GET_BY_ID, SCHEMA_POST_QUERY] customApiNames: [shippingCost] systemApiNames: [] webSocketEventNames: [] ``` **src/api_users/shop_app/shop_app.yaml** ```yaml name: shop_app username: shop_app groupNames: [Shop app] enablePasswordPath: true apiUserPasswordPath: common.apiUserPasswords.shop_app # where the password is in the secret : never write password ``` **src/instances/mongodb/mongodb.yaml** ```yaml name: mongodb type: MONGO_DB # MYSQL_DB, MARIA_DB, POSTGRE_SQL_DB, SQL_SERVER_DB, ORACLE_DB connectionString: common.connectionString.mongodb # a path in the secret, never the string itself connectionStringSandbox: common.connectionString.mongodb # the one the sandboxes use maxPoolConnections: 10 ``` - Secrets are not in the repository : ask the person to add what a path names (`common.apiUserPasswords.shop_app`) to the secret of each environment. See [Secrets](https://docs.apimaker.dev/v4/docs/secrets/secrets.kotlin.md) and [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md). ## Texts, log profiles, saved requests - **Texts of a language** (`src/i18ns//`) : `.yaml` (`name`) and `.ts`, `module.exports = { "message": "translation" }`. See [Internationalization](https://docs.apimaker.dev/v4/docs/i18/i18.kotlin.md). - **Log profiles** (`src/log_profiles//.yaml`) and the shared saved requests of API Testing (`src/api_testing_states//` : YAML, `.code.ts`, `.response.txt`) : made in the admin panel, kept as they are. ## Packages and sandboxes `src/admin_user/data.yaml` holds the sandboxes of the account : the runtime, the version and the packages of each family of languages (`javascript`, `python`, `php`, `dotnet`, `go`, `rust`, `java`, `kotlin`), under `settings.sandbox.environments`. A package of a new library goes there : **src/admin_user/data.yaml (a part)** ```yaml settings: sandbox: environments: python: packages: - name: requests version: 2.32.3 ``` - The Dockerfile written for a family is `src/admin_user/sandbox//Dockerfile`. See [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#packages). ## SSR website `src/ssr//` is a project of TypeScript : `website.yaml`, `site.config.ts`, `pages/`, `components/`, `public/`. Read the [guide for AI assistants of the SSR websites](https://docs.apimaker.dev/v4/docs/ssr/ai-guide.md) first. ## Related - [Git integration](https://docs.apimaker.dev/v4/docs/Git/git.md) · [Docs for AI assistants](https://docs.apimaker.dev/v4/docs/ai/llms.md) · [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) · [Custom API examples](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) --- # All APIs at a Glance > Every API of API Maker on one page - the 17 operations of a table under /api/schema and /api/gen (14 on SQL tables) with their method and URL, custom APIs, hooks, events, schedulers, the 23 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 API Maker gives every table of your databases ready-made APIs. You add your own APIs in code. This page is the map : each API has its own page, with examples. > **In short** > > - Every table of every database you connect gets ready-made APIs : 17 on MongoDB, 14 on SQL tables. > - **Schema APIs** (`/api/schema/…`) check each write against the schema of the table. **Generated APIs** (`/api/gen/…`) do not. > - Your own APIs are **custom APIs** (`/api/custom-api/…`). Ready-made tools are **system APIs** (`/api/system-api/…`). > - Every call sends a token in `x-am-authorization`, and gets `{ success, statusCode, data }` back. [![Instance API : the instance, the database, the table products and the list of its APIs](https://docs.apimaker.dev/assets/images/screens/ref-apis.webp)](https://docs.apimaker.dev/assets/images/screens/ref-apis.webp) 1. How many APIs the table has. 2. Each API of the table : its method and its URL. **Test** opens it in API Testing. > 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. - Video: [Instant REST APIs for Every Table: A Tour of the Generated APIs](https://apimaker.dev/videos/generated-apis-tour) (5 min) ## 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.kotlin.md) 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.md) 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 - Video: [Connect 6 Databases and Get REST APIs for Every Table](https://apimaker.dev/videos/connect-databases) (7 min) **APIs & Logic → Instance API (DB API)** has four columns, left to right : instances, databases, tables, APIs. Pick one in each, and the next column fills. - **Instance** : one database server that API Maker connects to. MongoDB, MySQL (also TiDB and Percona), MariaDB, SQL Server, PostgreSQL or Oracle. Its connection string stays in the [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.kotlin.md). - **Database** and **table** : what that connection sees, with their [settings](https://docs.apimaker.dev/v4/docs/settings/databaseSettings.md) and [schemas](https://docs.apimaker.dev/v4/docs/schema/schema.md). - **API** : one of the operations below, with its [settings](https://docs.apimaker.dev/v4/docs/settings/apiSettings.md) and a **Test API** button. The name of an instance is part of every URL of its APIs. You can rename it, but then every one of those URLs changes. Each button of these columns is explained on the [table designer](https://docs.apimaker.dev/v4/docs/schema/table-designer.md#panels) page. ## The 17 operations of a table A MongoDB collection has all 17. A table of an SQL database has 14. Each path below comes after `/api/schema////
`, or the same under `/api/gen`. `[/:primaryKey]` is optional : it names the column to find the id in, when it is not the primary 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.md) | | 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.kotlin.md) | | 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.md) | | Save single or multiple | POST `/save-single-or-multiple` | one object or an array ; `?returnData=false` | [Save single or multiple](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-save-single-or-multiple-api.md) | | Master save | POST `/master-save` | objects with nested related objects ; `?returnData=false` | [Master save](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-master-save-api.md) | | 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.md) | | Update many | PUT `/update-many` | `{ find, updateData, returnData? }` | [Update many](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-many-api.md) | | 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.md) | | 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.md) | | 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.md) | | Remove by query | POST `/query/delete` | `{ find, returnData? }` | [Remove by query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-query-api.md) | | 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.md) | | 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.kotlin.md) | | Aggregate (MongoDB) | POST `/aggregate` | a pipeline array | [Aggregate](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-aggregate-api.md) | | Count | POST `/count` | `{ find }` | [Count](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-count-api.md) | | Distinct | GET `/distinct/:field[/:order]` | | [Distinct](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-api.md) | | 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.md) | - **Replace by id, array operations and aggregate are for MongoDB only.** On an SQL table they answer `400` : `This API is only supported for mongodb.` (replace by id says `Replace by id API is not supported for instance type 'MYSQL_DB'.`). - **A schema API needs a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md).** On a table without one, it answers `400` : `Schema is not defined for this collection 'products'. You can define schema or please try /api/gen version of the api.` The generated APIs work on every table. - **Each operation has an id** : `SCHEMA_GET_ALL`, `GEN_POST_BULK_INSERT`… Settings, groups, hooks and WebSocket events use it. See [API ids](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#api-ids). ## What a write answers : returnData A write that touches many rows answers a count by default. It reads the rows back only when you ask for them. | API | Answer by default | With `returnData` | |---|---|---| | Save single or multiple, master save | the saved rows | `?returnData=false` : `{ savedRowsCount }`, and the rows are not read back | | Update many | `{ updatedRowsCount }` | `"returnData": true` in the body : `updatedRows` too, as they are after the update | | Remove by query | `{ deletedRowsCount }` | `"returnData": true` in the body : `deletedRows` and `ids` too, read before they go | From code, `returnData` is a key of the `g.sys.db` call in every language : `return_data` in Python and Rust, `ReturnData` in C# and Go. ## Shared by every read | | | |---|---| | [Query params](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md) | `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.kotlin.md) | 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.kotlin.md) | `{ success, statusCode, data, totalCount, errors, warnings, logs, meta }` | ## Your own APIs | Family | Method and path | Page | |---|---|---| | Custom API | GET POST PUT DELETE `/api/custom-api//` : the method and path you give it | [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md) | | Pre and post hooks | Your code that runs before or after an API. | [Pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) | | Events | Your code that runs after a successful call of the APIs you pick, or when code emits the event. | [Events](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.kotlin.md) | | WebSocket events | Messages pushed to the clients that subscribed. | [WebSocket events](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.kotlin.md) | | Schedulers | Your code that runs at intervals or on a cron. | [Schedulers](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md) | | Utility classes | Code shared by all of the above : `import * as Tax from 'utils/Tax'`. | [Utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.kotlin.md) | ## System APIs Ready-made tools : 23 APIs under `/api/system-api//…`. All are POST. Your code calls the same tools through `g.sys.system` and `g.sys.cache`. | Group | APIs | |---|---| | Security | [`/token`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.kotlin.md) · [`/encrypt-data`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-encrypt-data-api.kotlin.md) · [`/decrypt-data`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-decrypt-data-api.kotlin.md) · [`/hash-data`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-hash-data-api.kotlin.md) · [`/get-secret-by-name`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-secret-by-name-api.kotlin.md) | | Redis and cache | [`/get-redis-key`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-redis-key-api.kotlin.md) · [`/set-redis-key`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.kotlin.md) · [`/remove-redis-key`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-remove-redis-key-api.kotlin.md) · [`/reset-redis-cache-db`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-db-api.kotlin.md) · [`/reset-redis-cache-custom-apis`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.kotlin.md) · [`/reset-redis-cache-system-apis`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-system-api.kotlin.md) | | Databases | [`/get-table-meta`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-table-meta-api.kotlin.md) · [`/create-indexes`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.kotlin.md) · [`/drop-indexes`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-drop-indexes-api.kotlin.md) · [`/get-indexes`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-indexes-api.kotlin.md) · [`/multi-tenant-instance-updated`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-multi-tenant-instance-updated-api.kotlin.md) · [`/execute-plain-query`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-execute-plain-query-api.md) · `executeQuery` from [code](https://docs.apimaker.dev/v4/examples/sys/system/executeQuery.kotlin.md) | | Events | [`/emit-event`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-api.kotlin.md) · [`/emit-event-ws`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-ws-api.kotlin.md) | | Validation | [`/is-valid-data-for-table`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-table-api.kotlin.md) · [`/is-valid-data-for-custom-api`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-custom-api.kotlin.md) · [`/is-valid-connection-string`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-connection-string-api.kotlin.md) | | Outside | [`/call-external-api`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.kotlin.md) | ## 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.kotlin.md) : `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.kotlin.md). ```kotlin import am.DeepItem import am.G import am.GetAll fun main(g: G): Any? { val products = g.sys.db.getAll( GetAll("mongodb", "shop", "products").find(mapOf("status" to "ACTIVE")).limit(10).deep(listOf(DeepItem("category"))), ) return products } ``` ## 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 - 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 --- # Response Format > What every API Maker API answers - the envelope with success, statusCode, data, totalCount, errors and warnings, how the status code is chosen, the error entries, and how headers change the format of the answer. Source: https://docs.apimaker.dev/v4/docs/apis-all/response-format.html Every API of API Maker answers with the same JSON object : the envelope. So one piece of client code can read every answer. > **In short** > > - `success` and `statusCode` are always there. `data` holds the result. > - A failed call has `errors` : one entry for each problem. > - The first error decides the status code of the answer. > - Headers can change the format (XML, YAML, plain text) and the case of the keys in `data`. ## The envelope **A successful answer** ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 1, "first_name": "Bob", "last_name": "Lin" } ], "totalCount": 5 } ``` - `data` holds the result. `totalCount` comes only with `getTotalCount=true`. - A key with nothing to say is not sent : `warnings` comes only when there are some. **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 } ] } ``` - `errors` lists every problem found. Each has a `code` and a `message`. A value the schema refuses also names its `field` and the `type` of the rule. | Key | When | What it holds | |---|---|---| | `success` | always | `true` when the call worked. | | `statusCode` | always | The HTTP status of the answer, also in the body. | | `data` | on success | The result : a list for get all and the queries, one row for get by id, whatever a custom API returns. `null` when there is nothing. | | `totalCount` | with `getTotalCount=true` | How many rows match the filter, before `skip` and `limit`. | | `errors` | on failure | One entry for each problem. See [An error entry](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#an-error-entry). | | `warnings` | when your code adds some | Problems that did not stop the call. A hook or a custom API adds them to `g.res.warnings`, in the shape of an error. | | `meta` | with `x-am-meta: true`, in the admin panel only | How the call ran : `executionTime`, `executionTimeMS`, `apiAccessGroups` (the groups that allowed it) and `runBy` (the server, process and sandbox that ran the code). | | `encryptedData` | with [`x-am-get-encrypted-data`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-get-encrypted-data) | The answer, encrypted. `get_only_encryption` sets `data` to `null`. | What your code writes with `g.logger` is never part of the answer. It goes to the log of the request, in [Log Explorer](https://docs.apimaker.dev/v4/docs/logs/logs-and-dashboards.md#log-explorer). ## An error entry **A row of a save breaks a rule of the schema** ```json { "type": "min", "field": "price_cents", "message": "Please provide minimum '0' for 'price_cents' field.", "code": 400 } ``` | Key | What it holds | |---|---| | `code` | The status of this problem. | | `message` | What went wrong. With [`x-am-internationalization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-internationalization), the message comes in the language of the caller. See the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md). | | `type` | For a value the schema refuses : the rule. `required`, `min`, `max`, `minLength`, `maxLength`, `emailNotValid`, `enumValidation`, `unique`, `invalidValue` (a wrong type, an error thrown by a `validatorFun`…), `schemaKeyNotFound` (a key the schema does not have), `virtualFieldUsedInFind`. | | `field` | For a value the schema refuses : the column, with dots for nested fields (`address.city`). | | `dataIndex` | For a `unique` error in a save of many rows : the place of the row in the list you sent, from `0`. | | `stack` | For an error of your code : the stack, one line in each entry. | | `apiCallSequence` | When API calls go round in a circle (`a` calls `b`, `b` calls `a`) : the APIs of the circle. | ## Status codes The status of a failed answer is the `code` of its **first** error. | Code | When | |---|---| | `200` | The call worked. | | `201` | Save single or multiple and master save saved the rows. | | `400` | The request is wrong : a value the schema refuses, an unknown instance, a bad query, a missing body. | | `401` | No token, a token that is not valid any more, or an API closed to the outside (`NO_ACCESS`). Get a new token from the [token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.kotlin.md). | | `403` | The token is good, but no group of the API user allows this API. Or the browser page is on an origin the account does not allow. | | `404` | No API at this URL and method. Also update by id, replace by id and remove by id with `throwErrorIfRecordNotFound=true`, when no row has the id. | | `500` | An error of your code or of the database. Also any error whose `code` is missing or is not one of the codes above. | A custom API can answer another status with [`g.res.statusCode`](https://docs.apimaker.dev/v4/examples/res/res.kotlin.md#status-code). ## The format of the answer The answer is JSON. These [headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) change it : | Header | Effect | |---|---| | `x-am-content-type-response` | `text/xml` or `text/yaml` : the same envelope, in XML or YAML. `text/plain` or `text/html` : when `data` is a text, the answer is that text alone. Errors stay JSON. | | `x-am-response-case` | The case of the keys in `data` : `camelCase`, `snakeCase`, `pascalCase`… Any other value, like `noChange`, keeps them as they are. See [the values](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-response-case). | | `x-am-response-object-type: make_flat` | Nested objects become one level, joined by `_` : `state_id_state_name`. | | `x-no-compression: true` | Never compressed. Without it, an answer bigger than `compressThreshold` (51200 bytes by default) is compressed in a way the client accepts : gzip, brotli or deflate. | A custom API can also answer a file or a page of its own : see [Content types](https://docs.apimaker.dev/v4/examples/res/res.kotlin.md#content-types). ## Response headers | Header | Meaning | |---|---| | `x-am-data-source` | `cache` when the answer came from the [cache](https://docs.apimaker.dev/v4/docs/features/automatic-caching.md), `api` when the API ran. Always `api` for the `/stream` APIs. | | `x-am-request-id` | The id of the request. Search for it in Log Explorer. | | `Content-Encoding` | `gzip`, `br` or `deflate` when the answer was compressed. | ## In your code A call of `g.sys.db`, `g.sys.system` or `g.sys.cache` returns `data`, and throws the `errors`. Ask for the full response to get the envelope instead : the second argument `true` in TypeScript, `full` in the other languages. Then nothing is thrown. ```kotlin val full = g.sys.db.full.getAll(GetAll("mongodb", "shop", "products")) if (!full.isSuccess) g.logger.error(full.errors) val rows = full.data ``` --- # Request Headers > Every request header API Maker understands - the tokens, the case and format of the answer, flat objects, metadata, caching, encryption of the body and of the answer, the language of the messages, the tenant, the sandbox, the debugger and compression - with an example of each. Source: https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html A header changes how API Maker answers a call, without changing the URL. The headers work on every API : database, custom and system APIs. > **In short** > > - `x-am-authorization` carries the token of the app that calls. Most calls need it. > - The other headers are optional. Leave them out and you get JSON, with the keys as they are. > - `x-am-meta` works in the admin panel only. - Video: [Shape Every API Answer with Request Headers: camelCase, XML, YAML, Flat](https://apimaker.dev/videos/request-headers) (5 min) ## All headers | Header | Values | What it does | |---|---|---| | [`x-am-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-authorization) | a token | The API user : the app that calls. Needed unless the API is public. | | [`x-am-user-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-user-authorization) | a token | The person who uses the app, from a DB token generator. | | [`x-aws-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-aws-authorization), [`x-google-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-google-authorization), [`x-azure-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-azure-authorization), [`x-custom-authorization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-custom-authorization) | a token | The person, from AWS Cognito, Google, Azure AD or your own provider. | | [`x-am-response-case`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-response-case) | `camelCase`, `snakeCase`… | The case of the keys in `data`. | | [`x-am-content-type-response`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#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.kotlin.md#x-am-response-object-type) | `no_action`, `make_flat` | Turns nested objects into one level. | | [`x-am-meta`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-meta) | `true`, `false` | Adds how the call ran, in `meta`. Admin panel only. | | [`x-am-cache-control`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-cache-control) | `no_action`, `reset_cache` | Skips the cached answer and refreshes it. | | [`x-am-get-encrypted-data`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-get-encrypted-data) | `no_encryption`, `get_only_encryption`, `get_data_and_encryption` | Encrypts the answer. | | [`x-am-encrypted-payload`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-encrypted-payload) | `true` | Says that the body is encrypted. | | [`x-am-internationalization`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md#x-am-run-in-sandbox) | `0`, `1`, `2`… | Which sandboxes may run the code. | | [`x-am-run-in-debugger`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-run-in-debugger) | `true` | Runs the code in the debugger. | | [`x-no-compression`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-no-compression), [`accept-encoding`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#accept-encoding) | `true` / `gzip`, `br`, `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.md) : the app that calls. The [groups](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) of that API user decide which APIs, tables and fields the call may reach. ```text x-am-authorization: eyJhbGciOiJIUzI1NiIsInR1BydlRrblJlcS ``` - Get the token from the [token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.kotlin.md). It lasts `jwtOptions.expiresIn` seconds : 72 hours by default. `expiresInSeconds` in the token request changes it. - No token, or a token that is not valid : `401`. A good token, but no group allows the API : `403`. - A public API (`apiAccessType: IS_PUBLIC` in its [settings](https://docs.apimaker.dev/v4/docs/settings/apiSettings.md)) does not need it. - Your code reads the API user in `g.req.auth.authAMUser`. ## x-am-user-authorization The token of a **person** : a row of your own users table. A DB token generator ([auth provider](https://docs.apimaker.dev/v4/docs/authorization/AMDB.md)) makes it, through the token API. ```text x-am-user-authorization: eyJhbGciOiJIpXVCJBydlRrblJlcS ``` - It is needed when the settings of the database, the table, the API or the custom API name that provider in `authProviders`. - Your code reads the person in `g.req.auth.authAMDB` : the columns of the user that the generator selects. - A token made for a tenant works for that tenant only. See [Multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.kotlin.md#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.md#aws) is set up. Your code reads it in `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.md#google) is set up. Your code reads it in `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.md#azure) is set up. Your code reads it in `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.kotlin.md) : your own code makes and checks the token. Your code reads what the check returns in `g.req.auth.authCustom`. ```text x-custom-authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ``` ## x-am-response-case Changes the case of the keys in `data`, also inside nested objects and lists. The keys of the envelope (`success`, `statusCode`…) do not change. 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 ``` **data** ```json [ { "id": "6713c2f8e4b0a1d2c3f4a5b6", "firstName": "JOHN", "lastName": "DOE" } ] ``` - The `_id` of MongoDB becomes `id` in `camelCase`, as in the answer above. - A value that is not in the table keeps the keys as they are. ## x-am-response-object-type Turns nested objects into one level, with `_` between the names. Use it for grids, CSV files and spreadsheets, for example after a [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md). 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 : how the call ran. Default : `false`. > **The admin panel only** > > `meta` shows the inside of your servers : their processes, the ports of the sandboxes, the groups that let a caller in. So API Maker gives it to the [API Testing](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#api-testing-page) page of the admin panel only. The request must come through the encrypted channel of the panel, with the token of an administrator. Every other caller gets the answer without `meta`, whatever the header says. A value other than `true` or `false` is still refused with `400`. **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, "runtime": "node", "runOnNativeProcess": false } ] } } ``` | Key | What it holds | |---|---| | `executionTime`, `executionTimeMS` | The time spent in API Maker, without the network. | | `apiAccessGroups` | The groups of the API user, and which one allowed the call. | | `runBy` | For a call that runs code (a custom API, a hook) : the server and process, the runtime, and the port of the sandbox. Code on the native process has `runOnNativeProcess: true` and `port: null`. Code of a [group of separate sandboxes](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.md#separate-sandbox) also shows `enableSeparateSandboxForThis` and `allowAllPackagesOfAdmin`. A call handed to one of the first sandboxes ([`x-am-run-in-sandbox`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-run-in-sandbox)) shows twice : once with `publishToRedis: true` for the hand-over, once for the sandbox that ran it. | ## x-am-internationalization The name of a language of [i18N Management](https://docs.apimaker.dev/v4/docs/i18/i18.kotlin.md). Every message of the answer comes in that language : the messages of API Maker, and your own (`errorList`, the texts your code throws). `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": "कृपया आईडी पैरामीटर का मान प्रदान करें." } ``` Add a language or change a message : the next call uses it. Nothing restarts. ## x-am-run-in-sandbox Which sandboxes may run the code of this call : a custom API, the hooks, the events. - Video: [Separate Sandboxes for Heavy APIs: Bun, Sandbox Count and Restarts](https://apimaker.dev/videos/separate-sandboxes) (9 min) ```text x-am-run-in-sandbox: 0 # any sandbox (the default) x-am-run-in-sandbox: 1 # always the first sandbox x-am-run-in-sandbox: 2 # one of the first two ``` - Use `1` when your code keeps something in memory, like a connection that a [process initializer](https://docs.apimaker.dev/v4/docs/features/process-initializers.kotlin.md) opened. Every call then meets the same runner, so `g.shared` is the same for all of them. - "The first" means the oldest sandboxes of the code : the ones of its language and runtime, or of its [group of separate sandboxes](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.md#separate-sandbox). How many sandboxes there are is a [setting](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#sandboxes-on-the-servers). - The call does not start a sandbox of its own : it uses the ones that run (one is started if none runs). It asks for `5` and only two run ? It uses those two. - `0`, or a value that is no number : any sandbox. - When the sandboxes [restart](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#restart-every), the first one is the oldest that still takes calls. No call fails for it. - Code with `runOnNativeProcess` runs in no sandbox : the header changes nothing for it. ## x-am-run-in-debugger ```text x-am-run-in-debugger: true ``` The code of the call stops at your breakpoints. It works while debugging is on for your account, and the API testing page is open in the admin panel. That page sends the header by itself. See [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## x-am-content-type-response The format of the answer. Default : `application/json`. | Value | The answer | |---|---| | `application/json` | `{ "success": true, "statusCode": 200, "data": [ … ] }` | | `text/xml` | The same envelope in XML. Each item of a list is an `<_el>`. | | `text/yaml` | The same envelope in YAML. | | `text/plain`, `text/html` | When `data` is a text : that text alone. Anything else stays JSON. | | `application/octet-stream` | When `data` is a base64 text (a `data:…;base64,` URL works too) : the bytes it stands for. Anything else stays JSON. | **text/xml** ```xml true 200 <_el> 4 JOHNNY ``` - Errors are never plain text or bytes. - The `/stream` APIs answer JSON, whatever this header says. - A custom API can choose the format itself with `g.res.contentType`. See [Content types](https://docs.apimaker.dev/v4/examples/res/res.kotlin.md#content-types). ## x-am-cache-control For an API with [caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.md) on. Default : `no_action`. | Value | What happens | `x-am-data-source` of the answer | |---|---|---| | `no_action` | The cached answer comes back from Redis, when there is one. | `cache` | | `reset_cache` | The API runs against the database. Its answer replaces the cached one. | `api` | ## x-am-get-encrypted-data Encrypts the answer with the transfer key of the [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.kotlin.md) (`encryptionAlgorithmFETransfer` and `secretFETransfer`) : the key you share with your web 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 app decrypts `encryptedData` with the same algorithm and key : AES, or the older RC4 or TripleDES. Anyone who has the app has that key : see [what it protects](https://docs.apimaker.dev/v4/docs/features/security-features.md#encrypted-request-payloads). ## x-am-encrypted-payload Says that the body is encrypted : `{ "dataEncFE": "…" }`. `dataEncFE` is `{ data, createdAt }`, encrypted with the transfer key of the secret. ```text x-am-encrypted-payload: true ``` - A body older than `feTransferDataValidityInSeconds` of the secret is refused : 15 seconds by default. - With `acceptOnlyEncryptedData: true` in the settings of an API, a plain body is refused. - See [Security features](https://docs.apimaker.dev/v4/docs/features/security-features.md#encrypted-request-payloads). ## 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.kotlin.md) instance. They work on the database of that tenant. ```text GET /api/schema/admin/crm/crm/customers x-am-tenant-username: acme ``` - It does what the tenant in the path does : `/api/schema/admin/crm::acme/crm/customers`. When a request does both, the path wins. - Each multi-tenant instance looks the tenant up in its own tenants table : the **Connection String Multi Tenant** of the instance. So one header serves every multi-tenant instance that a call uses. - An unknown tenant 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`. Its `g.sys.db` and `g.sys.system` calls pass it on, and so do the hooks. - Cached answers of custom and system APIs are kept per tenant. - A [token of a person](https://docs.apimaker.dev/v4/docs/features/multi-tenant.kotlin.md#users-of-a-tenant) made with this header works for that tenant only. - On an instance that is not multi-tenant, the header changes nothing. ## x-am-sandbox-timeout How long the code of this call may run, in milliseconds : a custom API and the hooks of its versions, or the hooks of an API. It counts wherever the code runs : in a sandbox, on the native process, or in the server itself. ```text x-am-sandbox-timeout: 60000 ``` - Without the header, a custom API gets its own `customApiTimeoutInSeconds`. Anything else gets `sandboxReqTimeout` of the [configuration](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.md) : 13000 ms. - The header wins over `customApiTimeoutInSeconds`. - When the time is over, the answer is the error `API didn't respond in 60000ms.` The runner of that code takes no new request, and ends once its other requests are done. - Event listeners, schedulers, migration scripts and process initializers have times of their own : see [Time of a run](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#time-of-a-run). ## x-no-compression API Maker compresses an answer of `compressThreshold` bytes or more : 51200 by default. With `true`, this answer goes out as it is. ```text x-no-compression: true ``` - A file that a custom API sends is compressed, whatever its size. - The `/stream` APIs are never compressed. ## accept-encoding The standard header. API Maker takes the first of `gzip`, `br` (Brotli) and `deflate` that the header accepts. So a browser that sends `gzip, deflate, br` gets gzip : it costs much less CPU than Brotli, for nearly the same size. ```text accept-encoding: br ``` `identity`, or no header, means no compression. The headers of the answer are listed in [Response format](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#response-headers). ## From your code The same headers go in `headers` of every `g.sys` call : ```kotlin val rows = g.sys.db.getAll( GetAll("mysql8", "inventory", "customers") .headers(mapOf("x-am-response-case" to "camelCase", "x-am-tenant-username" to "acme")), ) ``` --- # Error Messages Reference > Every message API Maker answers (AM_00001 to AM_00125), what causes it and what to check - schema rules, queries, deep and find join, tokens, secrets, encrypted bodies, instances, custom API files - and how to translate them with internationalization. Source: https://docs.apimaker.dev/v4/docs/apis-all/error-codes.html Each message of API Maker has a code, like `AM_00001`. The code helps you find the cause fast. You can also translate any message for your users with [internationalization](https://docs.apimaker.dev/v4/docs/i18/i18.kotlin.md). > **In short** > > - A message is a template : `{field}`, `{value}`… are filled from the request. > - The answer holds the message only, not its code. Look the text up on this page. > - Most of these messages come with `400`. The access messages come with `401` or `403`. - Video: [Custom APIs on One Standard: Validation, Standard Errors, Fast Fixes](https://apimaker.dev/videos/custom-api-standards) (10 min) **An answer with AM_00002** ```json { "success": false, "statusCode": 400, "errors": [ { "type": "min", "field": "price_cents", "message": "Please provide minimum '0' for 'price_cents' field.", "code": 400 } ] } ``` ## Data and schema The [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) of the table refused a value. | Code | Message | When | |---|---|---| | AM_00001 | Please provide valid '{field}' field with type '{type}'. | A required field is missing or empty, in a new row. | | 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}'. | `validations.unique` : the value is already in the table, or twice in the rows you send. | | AM_00007, AM_00008 | Please provide unique combination for keys ['{validationPath}'] in data index '{dataIndex}'. | A unique combination of columns, already there or twice in the rows you send. | | AM_00010 | {field} should be array in schema as data is in array. | A list sent for a column that holds one value. | | AM_00011 | '{value}' is not a valid number for key '{field}'. | A value that can not become the number the schema asks for. | | AM_00015 | Invalid date in '{field}' field. | A value that can not become a date. | | AM_00017, AM_00018, AM_00019 | Can not convert object / array to objectId in '{field}' field. Invalid objectId in '{field}' field. | A value that can not become an ObjectId. | | 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_00058 | Found empty schema to validate. | The schema of the table has no columns. | | 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 a list of objects. | | AM_00062 | Type of '{field}' should be array in schema. | [Array operations](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-array-operations-api.md) on a column that is not a list in the schema. | | AM_00087 | Please provide valid value in {field}. | A `validatorFun` returned `false`. | | AM_00101 | {message} | The text your code threw : a `validatorFun`, a `conversionFun`… | | AM_00111 | Property '{field}' should be valid email address. | `validations.email` | | AM_00114 | [{field}] is duplicate having value [{value}]. | A unique index of the database refused the row. | | 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 you sent is old, or missing : someone saved the row since. See [optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.md). | ## Queries A `find`, a `select`, an id or a body that API Maker can not use. | Code | Message | When | |---|---|---| | AM_00009, AM_00020 | {field} should be array as it is logical operator. | `$and`, `$or` or `$nor` with something that is not a list. | | AM_00012 | Please provide id param value | Get by id, update by id or remove by id, without an id. | | AM_00013, AM_00014 | Invalid value in '{field}' field, expected number… / Invalid number in '{field}' field. | A `find` value that can not become the number of the column. | | AM_00023, AM_00024 | Array is expected in key '{field}'. / … and it's length is zero. | An operator like `$in` without a list, or with an empty one where a value is needed. | | AM_00044 | Please provide array for aggregate body. | The [aggregate API](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-aggregate-api.md) needs a pipeline : a list. | | AM_00045, AM_00047 | Please provide field for distinct API. You can also provide multiple fields separated by comma. | Distinct without a column. | | AM_00046, AM_00048 | Invalid column ['{field}'] found in your field list. | Distinct with a column the schema does not have. | | AM_00049 | Please provide 'find' property having query to remove data. | [Remove by query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-query-api.md) without a `find`. Nothing is ever deleted without a filter. | | AM_00051 | Primary key '{field}' not found in schema. | The `primaryKey` part of the URL names a column the schema does not have. | | AM_00052, AM_00053 | Unable to convert '{value}' to number / objectId based on your schema type. | An id that does not fit the type of the key. | | AM_00054, AM_00055 | Invalid primary key type '{type}' found for '{field}'. / Primary key not found in … | The table has no primary key API Maker can use. | | 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 on a table without a schema. | | AM_00060 | find property should be string of valid JSON/JSON5. | A `find` query param that does not parse. | | AM_00065 | Unsupported value found for parameter '{field}' | SQL Server got a value of a kind it has no type for. | | AM_00097 | Please provide 'path' property. | An item of array operations without its `path`. | | AM_00098 | Please provide request body. | A write without a body. | | AM_00112 | Please provide valid value in '{field}'. It should be array in schema. | A `find` value for a list column that is not a list. | | AM_00119 | Record not found. | `throwErrorIfRecordNotFound=true` and no row has the id : `404`. | | AM_00042, AM_00095 | This API is only supported for mongodb. / Replace by id API is not supported for instance type '{type}'. | Aggregate, array operations or replace by id on an SQL table. | ## Deep and find and join [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) and [find and join](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md#find-and-join-fields-of-related-tables) could not reach the related table. | Code | Message | When | |---|---|---| | AM_00036 | Please provide t_col value. | A find and join on a column whose schema names no related table. | | AM_00037 | Unable to find collection for s_key:'{field}', You can provide it in schema or deep by 't_col' property. | A `deep` item without `t_col`, and no relation in the schema for its `s_key`. | | AM_00039 | Unable to find collection with name '{collectionName}' on path '{instanceName} -> {databaseName} -> {collectionName}'. | The related table does not exist. | | AM_00040 | Unable to find primary key for target collection '{collectionName}'… | The related table has no primary key, and the item has no `t_key`. | | AM_00063, AM_00066, AM_00067 | Unable to find schema … / Unable to find property '{field}' in schema of … | The related table, or its column, has no schema. | | AM_00085 | $in/$nin operator value must be non empty array. | A find and join with `$in` or `$nin` on something that is not a list. | | AM_00086 | $gt/$gte/$lt/$lte value should be valid [string, number, date (iso format)]. | A find and join that compares with something that is not a text, a number or a date. | | AM_00118 | Virtual field can not be used in find join feature. '{field}' is virtual field. | A find and join on a virtual field. | ## Tokens and access Who calls, and what they may do. See [Secure your APIs](https://docs.apimaker.dev/v4/docs/getting-started/secure-your-api.md). | Code | Message | When | |---|---|---| | AM_00035 | Authorization header 'x-am-authorization' is not valid. | No token of an API user, or a token that does not verify : `401`. | | AM_00084 | Invalid access token. | A token of an API user made before its password changed, or of another account : `401`. | | AM_00034 | User '{name}' with username '{username}' is not having access to this API '{url}'. | A good token, but no group of the API user allows this API : `403`. | | AM_00050 | User '{name}' does not have access to fields [{field}]. | The groups of the API user do not allow these columns. | | AM_00113 | You are not authorized to access this API. | The API is closed to callers from outside (`NO_ACCESS`) : `401`. | | AM_00032 | Invalid request origin. You can give access to this host '{origin}' from settings. | A token that the admin panel made for API Testing, used outside the admin panel. | | AM_00090 | Please provide token request with ['u', 'p'] or ['refresh_token']. | The body of the [token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.kotlin.md) lacks the username and password. | | AM_00092 | Please provide '{name}' token in request headers. | The API asks for the token of a person, and the header is missing. | | AM_00093 | Invalid token provided in '{name}' header. | The token of a person does not verify. | | 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_00115 | Invalid JWT token provided. | A token of a person, made before the password of that person changed. | ## Secrets and encryption The [default secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.kotlin.md) lacks a key, or an [encrypted body](https://docs.apimaker.dev/v4/docs/features/security-features.md#encrypted-request-payloads) is not right. | Code | Message | When | |---|---|---| | AM_00025 | A default secret is required because of encryption used in key '{field}'. | A column with `encryption` or `hashing`, and 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']. | The secret names an algorithm API Maker does not have. | | AM_00027, AM_00029 | Unable to find encryption algorithm / secret for '{field}' field… | The default secret lacks `common.encryptionAlgorithm` or `common.secret`. | | AM_00028, AM_00030 | Unable to find hashing algorithm / nonce for '{field}' field… | The default secret lacks the keys of hashing. | | AM_00059 | Default secret is not present in the system. | [Get secret](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-secret-by-name-api.kotlin.md), and the account has no active default secret. | | AM_00103 | Unable to decrypt data of 'dataEncFE' field. | The body does not decrypt with the transfer key. | | AM_00104 | Please provide value of 'encryptionAlgorithmFETransfer' and 'secretFETransfer' in secret. | An encrypted body, and the secret has no transfer key. | | AM_00106 | Payload expired. | The body is older than `feTransferDataValidityInSeconds`. | | AM_00108 | Please provide field 'createdAt' with current time in ISO format… | The encrypted body has no `createdAt`. | ## Instances, custom APIs and files | Code | Message | When | |---|---|---| | AM_00031 | Instance with name '{instanceName}' does not exist. | The instance of the URL or of the call is not there. | | AM_00099 | Please provide valid API path. Can not find any user with path '{value}'. | No account has the user path of the URL. | | AM_00057 | Path '{url}' does not exist. | No custom API with this path and method : `404`. | | AM_00073 | Unable to find custom API with name '{name}'. | [Is valid data for custom API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-custom-api.kotlin.md) or [reset custom API cache](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.kotlin.md) names a custom API that is not there. | | AM_00096 | Unable to find system API with name '{name}'. | [Reset system API cache](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-system-api.kotlin.md) names a system API that is not there. | | AM_00100 | WebSocket event not found with name '{name}'. You can create this event in API Info → WebSocket Events. | [Emit WebSocket event](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-ws-api.kotlin.md) with an unknown event. | | AM_00068, AM_00069 | Invalid request. Please provide valid value for header x-am-code-hash. / Migration script does not found with id '{apiId}'. | Code tried from the admin panel, without the item it belongs to. | | AM_00120 | File upload is not supported in [{name}] API. | A file sent to a custom API whose settings do not take files. | | AM_00121, AM_00122 | Minimum / Maximum file size supported is '{value}' bytes and you provided [{name}] file with '{value2}' bytes in '{field}' field. | A file too small or too big for the settings of the custom API. | | AM_00123 | Allowed extensions are [{value}] and you provided file '{name}' with extension '{value2}'. | A file of a kind the settings do not allow. | | AM_00124 | Allowed fields are [{value}] and you have provided value in field '{field}'. | A file under a field name the settings do not allow. | | AM_00125 | Please provide ['oracleDBUsername', 'oracleDBPassword', 'oracleDBPrivilege']. | [Is valid connection string](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-connection-string-api.kotlin.md) for Oracle, without its user. | ## Messages API Maker 4 never sends i18N Management lists these templates too, but API Maker 4 does not answer them : AM_00016, AM_00064, AM_00070, AM_00071, AM_00072, AM_00074, AM_00075, AM_00076, AM_00077, AM_00078, AM_00079, AM_00081, AM_00082, AM_00107, AM_00110. ## Translate them 1. Open **APIs & Logic → i18N Management** and add a language. 2. Change the text of any code. Your own messages (`errorList` of a custom API, the texts your code throws) can have a translation too. 3. Send the name of the language in `x-am-internationalization`. Every message of the answer follows it. See [Internationalization](https://docs.apimaker.dev/v4/docs/i18/i18.kotlin.md). --- # Query Parameters > The query params of the APIs of a table in API Maker - find, skip, limit, sort, select, deep, getTotalCount, a column as a filter, upsert, returnDocument and throwErrorIfRecordNotFound - which API takes which, 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 take their options in the URL : `?limit=10&sort=-customer_id`. The query APIs take the same keys in their JSON body. > **In short** > > - `find` filters, `sort` orders, `skip` and `limit` make pages, `select` picks the columns, `deep` brings related rows. > - `getTotalCount=true` adds the total number of matching rows to the answer. > - Without `limit`, a read returns every row that matches. > - Values are JSON5 : `find={first_name:'Bob'}` works without quotes around the keys. The examples read this table, `customers` of the instance `mysql8`. | 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.kotlin.md) | `?find={first_name:'Bob',pincode:{$gte:382345}}` | Filters with the operators of MongoDB, on every database. | | a column | `?first_name=Bob,Alice` | A column name is a filter too. See [A column as a query param](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md#a-column-as-a-query-param). | | [skip](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#skip) | `?skip=20` | Leaves out the first rows. | | [limit](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#limit) | `?limit=10` | At most this many rows. | | [sort](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#sort) | `?sort=-last_update,first_name` | The order. `-` for descending. | | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.md) | `?select=first_name,last_name` | Only these columns. `-password` : all but this one. | | [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) | `?deep=state_id` | Puts the related row in place of its id. | | [getTotalCount](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#getTotalCount) | `?getTotalCount=true` | Adds `totalCount` : the rows that match, before `skip` and `limit`. | | [upsert](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#upsert) | `?upsert=true` | Inserts the row when the id is not there. | | [returnDocument](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#returnDocument) | `?returnDocument=before` | Answers the row as it was before the change. | | [throwErrorIfRecordNotFound](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#throwErrorIfRecordNotFound) | `?throwErrorIfRecordNotFound=true` | Answers `404` for an unknown id, in place of `data: null`. | ## Which API takes which | API | What it takes | |---|---| | Get all, get all by stream, distinct | Every read param of the URL, and columns as filters. | | Get by id | `select` and `deep`. | | Save single or multiple, master save | `select` and `deep`, which shape the answer, and [`returnData=false`](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#return-data). | | Update by id, replace by id | `select`, `deep`, `upsert`, `returnDocument`, `throwErrorIfRecordNotFound`. | | Remove by id | `select`, `deep`, `throwErrorIfRecordNotFound`. | | Query, query by stream | The same keys, in the JSON body. | | Count, distinct with query, update many, remove by query | Their own JSON body, with a `find`. See the page of each API. | ## skip Leaves out the first rows. With `limit`, it makes pages : 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 no `skip`, leaves out nothing. - Always `sort` when you make pages. Without an order, the database may send the rows in any order : a page may repeat a row or miss one. ## limit At most this many rows. ```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" } ] } ``` - Without `limit`, get all and the query APIs return every row that matches. Give a `limit` as soon as a table can grow. - To read a whole big table, use a stream ([get all by stream](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.kotlin.md)) : the rows go out as they come, without filling the memory of the server. ## 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, `sort` is a text or an object : ```json { "find": {}, "sort": "-last_update,first_name" } ``` ```json { "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.md) finds the queries that need an index. ## getTotalCount `getTotalCount=true` adds `totalCount` to the answer : how many rows match 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}&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, the query API and the two streams. A stream sends `totalCount` after the rows. - The count is a second query on the database. On a very big table with a hard filter, it costs as much as the query itself. - Need the number only ? Call the [count](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-count-api.md) API. ## 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.md) and [replace by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-replace-by-id-api.md) insert the row when the id is not there. ```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 | |---|---|---| | is there | It is updated with the body. | The row after the change. | | is not there | It is inserted : `customer_id: 999` and the body. | The new row. | - Without `upsert`, an unknown id changes nothing, and the answer is `data: null`. - An inserted row is checked like a new row : with a schema, every `required` column must be in the body. ## returnDocument Which version of the row update by id and replace by id answer : `after` the change (the default), or `before` it. It changes the answer only, never what is saved. ```text PUT /api/schema/admin/mysql8/inventory/customers/update-by-id/1?returnDocument=before ``` **Answer : the row before the new 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 in the answer. - With `upsert`, a row that was inserted had no version before : `before` answers `null`. ## throwErrorIfRecordNotFound For update by id, replace by id and remove by id. With `true`, an unknown id answers `404` with the message `Record not found.` Without it, the answer is `200` with `data: null`. ```text DELETE /api/schema/admin/mysql8/inventory/customers/999?throwErrorIfRecordNotFound=true ``` **Answer** ```json { "success": false, "statusCode": 404, "errors": [ { "code": 404, "message": "Record not found." } ] } ``` ## Write them right - Values are **JSON5** : `find={first_name:'Bob'}` needs no quotes around the keys, and takes single quotes. Strict JSON works too. - Encode the value for the URL when your client does not : `find=%7Bfirst_name%3A'Bob'%7D`. The API Testing page and `g.sys.db` do it for you. - In the body of the query APIs, the same keys are plain JSON : ```json { "find": { "first_name": "Bob" }, "sort": "-customer_id", "skip": 0, "limit": 10, "select": "first_name,last_name", "getTotalCount": true } ``` - In a [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) item, `skip`, `limit` and `sort` apply to the related rows of each row. - From code, `queryParams` of `g.sys.db.getAll` takes the same keys, as objects : see the [code examples](https://docs.apimaker.dev/v4/examples/sys/db/getAll.kotlin.md). - The schema APIs convert the values of `find` to the types of the schema : `"8"` matches the number `8`. The generated APIs compare what you send as it is. --- # 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. You write it the way MongoDB writes a filter. API Maker translates it for MySQL, MariaDB, PostgreSQL, SQL Server and Oracle too. So one filter works on every database. > **In short** > > - `{ "first_name": "Bob" }` : equal. `{ "pincode": { "$gte": 382345 } }` : an operator. > - `$and`, `$or`, `$nor` and `$not` combine conditions, on every database. > - A key with dots reaches a column of a related table : `{ "customer_id.loyalty_tier": "GOLD" }`. > - On SQL, every value is sent as a parameter of the statement, never as SQL. **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 | - Video: [One Query Language for Six Databases: find, select, sort, aggregate](https://apimaker.dev/videos/read-apis) (7 min) ## 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.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md#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.md) 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. - Video: [Filter by a Related Table, Even in Another Database: Find and Join](https://apimaker.dev/videos/find-and-join) (5 min) > 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 all by stream and distinct, a column name in the URL is a filter too. No `find` is needed. | Query param | The filter it makes | |---|---| | `?customer_id=1` | `{ "customer_id": 1 }` | | `?first_name=Bob,Alice` | `{ "first_name": { "$in": [ "Bob", "Alice" ] } }` | | `?first_name!=Bob,Alice` | `{ "first_name": { "$nin": [ "Bob", "Alice" ] } }` | | `?pincode>=382345` | `{ "pincode": { "$gte": 382345 } }`. Also `>`, `<`, `<=` and `!=`. | | `?first_name=/^ma/i` | A regular expression. | | `?last_update` | MongoDB : the rows that have the field. SQL : the rows where it is an empty text. | | `?!last_update` | The rows without the field. SQL : the rows where it is `NULL`. | - A value becomes what it looks like : `5` a number, `true` a boolean, `2024-02-29` a date. `007` stays a text, because of its first zero. - To say the type, wrap the value : `string(5)`, `number(5)`, `int(5)`, `boolean(true)`, `date(2024-02-29)`, `lowercase(ABC)`. - The columns and `find` apply together : `?first_name=Bob&find={pincode:382345}`. ## From code ```kotlin val rows = g.sys.db.query( Query("mysql8", "sales", "orders") .find(mapOf("total" to mapOf("\$gte" to 500), "customer_id.loyalty_tier" to "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.kotlin.md) narrows it to their own rows. The [APIs Security Report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.md) 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. --- # 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 columns you name. With `-`, it keeps everything but the columns you name. The answer is smaller, and holds only what your screen needs. > **In short** > > - `?select=first_name,last_name` : only these columns. `?select=-password` : all but this one. > - With a schema, the primary key always comes along. > - Columns the groups of the caller may not read never come back. **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 works too : `select=address.city`. - Do not mix kept and left-out columns. If you do, only the left-out ones count : `select=first_name,-pincode` returns every column but `pincode`. - On a schema API, a column the schema does not have stops the call : `Unable to find [nick_name] columns in schema of [mysql8 → inventory → customers].` - The write APIs (save, master save, update by id, replace by id, remove by id) take `select` too. It shapes the row they answer. - In a `deep` item, `select` picks the columns of the related rows : `deep=[{s_key:'customer_id',t_col:'orders',t_key:'customer_id',select:'order_no,total'}]`. - A column the groups of the caller may not read never comes back, selected or not. See the column permissions of [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md). ## 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.kotlin.md). 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.md) - [sort](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#sort) --- # deep Parameter (Deep Populate) > Get the row an id points to in place of the id with the deep parameter of API Maker - one column name when the schema knows the relation, any table, database or instance otherwise, several levels, one to many with isMultiple and virtual fields, find, select, sort, skip and limit per item. Source: https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.html A column such as `state_id` holds an id. With `deep`, the answer holds the row of that id instead, in the same call. > **In short** > > - Name the column : `?deep=state_id`. The schema knows where it points. > - No relation in the schema ? Name the table in an item : `t_col` and `t_key`. > - A `deep` inside an item goes one level further. > - API Maker reads related rows in bulk, never with one query per row. They can be on another database. > 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. - Video: [Deep Populate: Join Data Across Six Databases in One API Call](https://apimaker.dev/videos/deep-populate) (9 min) ## The tables of the examples | Table | Instance | Columns | Rows | |---|---|---|---| | `cities` | `postgresql` | `id`, `city_name`, `state_id` | 101 Ahmedabad → 201, 102 Surat → 201, 103 Pune → 202 | | `states` | `oracle` | `id`, `state_name`, `country_id` | 201 Gujarat → 301, 202 Maharashtra → 301 | | `countries` | `mysql` | `id`, `country_name` | 301 India | Each instance has a database named `geo`. The [schemas](https://docs.apimaker.dev/v4/docs/schema/schema.md#relations-and-virtual-fields) name the relations : **The relations** ```typescript // cities.schema.ts : state_id points to states.id, on the instance oracle state_id: { __type: EType.number, instance: 'oracle', table: 'states', column: 'id' }, // states.schema.ts : country_id points to countries.id, on the instance mysql country_id: { __type: EType.number, instance: 'mysql', table: 'countries', column: 'id' }, ``` ## Name the column ```text GET /api/schema/admin/postgresql/geo/cities?deep=state_id ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "id": 101, "city_name": "Ahmedabad", "state_id": { "id": 201, "state_name": "Gujarat", "country_id": 301 } }, { "id": 102, "city_name": "Surat", "state_id": { "id": 201, "state_name": "Gujarat", "country_id": 301 } }, { "id": 103, "city_name": "Pune", "state_id": { "id": 202, "state_name": "Maharashtra", "country_id": 301 } } ] } ``` In the body of the [query API](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-query-for-get-data-api.md), the same is `{ "deep": ["state_id"] }`. ## Name the table Without a relation in the schema, an item says where the id points. `s` is the source, this table. `t` is the target, the related table. ```text GET /api/schema/admin/postgresql/geo/cities?deep=[{s_key:'state_id',t_instance:'oracle',t_col:'states',t_key:'id'}] ``` The answer is the same as above. | Key | What it says | When you leave it out | |---|---|---| | `s_key` | The column of this table which holds the id. Required. | | | `t_col` | The related table. | The schema says it. | | `t_key` | The column of the related table which holds the same value. | The schema says it, else the primary key of the related table. | | `t_db` | The database of the related table. | The schema says it, else the database of this table. | | `t_instance` | The instance of the related table. | The schema says it, else the instance of this table. | What an item says wins over the schema. ## Go deeper A `deep` inside an item fills the rows that item brought. This is the call of the diagram : ```text GET /api/schema/admin/postgresql/geo/cities?deep=[{s_key:'state_id',deep:[{s_key:'country_id'}]}] ``` **First row of data** ```json { "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. - Short form : `?deep=state_id,state_id.country_id`. Order matters : `state_id.country_id` reads the rows `state_id` brought, so it comes second. ## Choose the related rows An item takes its own `find`, `select` and `sort`, like a query on its table : ```text GET /api/schema/admin/postgresql/geo/cities?deep=[{s_key:'state_id',find:{state_name:'Gujarat'},select:'state_name'}] ``` **data** ```json [ { "id": 101, "city_name": "Ahmedabad", "state_id": { "id": 201, "state_name": "Gujarat" } }, { "id": 102, "city_name": "Surat", "state_id": { "id": 201, "state_name": "Gujarat" } }, { "id": 103, "city_name": "Pune", "state_id": 202 } ] ``` - `select` keeps only these columns, plus `id`, the column the rows are matched by. - A state which `find` leaves out is not put in : Pune keeps `state_id: 202`. ## One to many : isMultiple `isMultiple: true` puts all the matching rows, as a list. Here, the cities of each state : ```text GET /api/schema/admin/oracle/geo/states?deep=[{s_key:'id',t_instance:'postgresql',t_col:'cities',t_key:'state_id',isMultiple:true}] ``` **First row of data** ```json { "id": [ { "id": 101, "city_name": "Ahmedabad", "state_id": 201 }, { "id": 102, "city_name": "Surat", "state_id": 201 } ], "state_name": "Gujarat", "country_id": 301 } ``` - Without `isMultiple`, only one city comes. - `sort`, `skip` and `limit` of the item work on the list of each state : `limit:1` keeps one city per state. - The list takes the place of the `id` of the state. To keep the id, use a virtual field. ### Virtual fields A [virtual field](https://docs.apimaker.dev/v4/docs/schema/schema.md#relations-and-virtual-fields) is in the schema only, not in the table. It stands for the rows of another table which point to this row : **states.schema.ts** ```typescript cities: { __type: EType.number, isVirtualField: true, instance: 'postgresql', table: 'cities', s_columnVirtualLinker: 'id', t_columnVirtualLinker: 'state_id' }, ``` ```text GET /api/schema/admin/oracle/geo/states?deep=[{s_key:'cities',isMultiple:true}] ``` **First row of data** ```json { "id": 201, "state_name": "Gujarat", "country_id": 301, "cities": [ { "id": 101, "city_name": "Ahmedabad", "state_id": 201 }, { "id": 102, "city_name": "Surat", "state_id": 201 } ] } ``` - A state without cities gets no `cities` field. - By default the states share one query, split only as in **Speed** below. `fetchingTechniqueSettings: { chunkSize: 100 }` asks for 100 states per query. - `fetchingTechnique: 'one_by_one'` asks one query per state, so the database itself applies `limit`. Use it with `sort` to keep a few rows out of many : the 5 latest orders of each customer. ## Good to know - **Where it works.** Get all, get by id, query, and the two stream APIs. The write APIs take `deep` too (save single or multiple, master save, update by id, replace by id, remove by id) : `POST …/cities/save-single-or-multiple?deep=state_id` answers the saved row with its state. Nothing more is saved. - **Speed.** API Maker asks each related table one query for all the rows. It splits the ids into more queries only when one statement can not hold them : 1,000 on Oracle, 2,000 on SQL Server, 60,000 on MySQL, MariaDB and PostgreSQL. Items on different columns are read at the same time. - **No row found.** An id without a related row stays as it is, even with `isMultiple`. So does an empty value. A list of ids becomes the list of the rows found. - **Security.** The related rows are read through the query API of their table, as the caller would read them. Columns the caller may not read are left out, and a table they may not read makes the item fail. The pre and post hooks of that API run, so a hook which keeps a person to their own rows does it in a `deep` too. API Maker 3 skipped these hooks. A `deep` from code (`g.sys.db`) runs them only with `skipHookRunning: false`. - **Errors.** On a read, an item with an unknown column or table answers `400`. `errors` names it, and `data` still holds the rows. See the [error codes](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md#query-deep-populate-and-find-join). - **Generated APIs** (`/api/gen`) read no schema : every item needs its `t_col`, and `t_key` is `_id` when left out. There, `deep` works from a MongoDB collection only. For an SQL table, call the schema APIs. - **Flat rows** for a grid or a CSV file : the header [`x-am-response-object-type: make_flat`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-response-object-type) turns `state_id: { state_name }` into `state_id_state_name`. - **Filter by a related column**, such as the cities of Gujarat : that is [find and join](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md#find-and-join-fields-of-related-tables), not `deep`. Both work in the same call. - **From code**, `deep` takes the same items : [examples in every language](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#deep-populate). --- # Get All API > Read the rows of a table with the get all API of API Maker - filter with find or a column, page with skip and limit, sort, select columns, bring related rows with deep, get the total count, from the cache when it is on. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.html Reads the rows of a table. With query params, it is the list API of your screens : filter, sort, pages, columns, related rows. > **In short** > > - `GET /api/schema////
` > - Without `limit`, it returns every row that matches. > - It takes every query param : `find`, a column, `skip`, `limit`, `sort`, `select`, `deep`, `getTotalCount`. | | | |---|---| | 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.kotlin.md), [a column](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md#a-column-as-a-query-param), [skip](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#skip), [limit](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#limit), [sort](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#sort), [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.md), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md), [getTotalCount](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#getTotalCount) | | Answer | `data` : a list of rows. `totalCount` with `getTotalCount=true`. | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, when `enableCaching` is on for the table or the database | | From code | [`g.sys.db.getAll`](https://docs.apimaker.dev/v4/examples/sys/db/getAll.kotlin.md) | | API id | `SCHEMA_GET_ALL` | | 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.md#without-a-schema) | ## The URL Put your own names in `/api/schema/admin/mysql8/inventory/customers` : the user path of your account (`admin`), the instance (`mysql8`), the database (`inventory`) and the table (`customers`). The examples read 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 } ] } ``` ## A page of a list Page 2 of the active customers : 2 rows a page, in the order of their id, two columns, and the total for the pager. ```text GET /api/schema/admin/mysql8/inventory/customers?find={isActive:1}&sort=customer_id&skip=2&limit=2&select=first_name,last_name&getTotalCount=true ``` ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 3, "first_name": "Mallory", "last_name": "Brown" }, { "customer_id": 4, "first_name": "Eve", "last_name": "Mathly" } ], "totalCount": 5 } ``` - `customer_id` comes along : with a schema, the primary key is always in the answer. - Each param has its page : [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md), [sort, skip, limit and getTotalCount](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md), [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.md). ## Short filters A column name in the URL filters too : ```text GET /api/schema/admin/mysql8/inventory/customers?first_name=Bob,Alice GET /api/schema/admin/mysql8/inventory/customers?pincode>=382346 ``` See [A column as a query param](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md#a-column-as-a-query-param) for every form. ## Related rows with deep `deep` puts the related rows in the answer. Here, the orders of each customer : ```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": [ { "order_no": 1001, "total": 12900, "customer_id": 1 } ], "first_name": "Bob", "last_name": "Lin" } ] } ``` The orders take the place of `customer_id`. To keep the id, use a [virtual field](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md#virtual-fields). Every option of `deep` is on its [page](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md). ## Good to know - **Cache.** With [caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.md) on, the answer comes from Redis until a write through API Maker changes the table. The groups of the caller are checked first, but the hooks do not run for a cached answer. `x-am-cache-control: reset_cache` reads the database again. - **Hooks.** A [pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) can add conditions to `g.req.query.find`, for example the rows of the person who calls. A [post hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) can change `g.res.output`. - **Big tables.** To read every row of a big table, use [get all by stream](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.kotlin.md). ## From your code [`g.sys.db.getAll`](https://docs.apimaker.dev/v4/examples/sys/db/getAll.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## 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` | | From code | [`g.sys.db.gen.getAllGen`](https://docs.apimaker.dev/v4/examples/sys/db/getAll.kotlin.md#without-a-schema) | A filter matches the value as you send it : `"22"` does not match the number `22`. On MongoDB, a text that looks like an ObjectId is turned into one. `deep` needs its `t_col`, and works from a MongoDB collection only. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # Get All by Stream API > Stream the rows of a table with the get all by stream API of API Maker - the filters of get all, rows sent as they are read, for exports and big tables, without filling the memory of the server. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.html The query of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md), answered as a stream. The rows go to the client as the database sends them. So a million rows cost the server a few kilobytes of memory, not the whole list. > **In short** > > - `GET /api/schema////
/stream` > - The same query params as get all. > - Use it for exports, reports and copies of a table. > - Never cached. Pre hooks run, post hooks do not. | | | |---|---| | Method | GET | | URL | `/api/schema/admin/mysql8/inventory/customers/stream` | | Body | none | | Query params | the ones of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md) : find, a column, skip, limit, sort, select, deep, getTotalCount | | Answer | `data` : a list of rows, sent as they come | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no : a stream always reads the database | | From code | [`g.sys.db.getAllByStream`](https://docs.apimaker.dev/v4/examples/sys/db/getAllByStream.kotlin.md) | | API id | `SCHEMA_GET_ALL_STREAM` | | 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.kotlin.md#without-a-schema) | ## The call The examples read the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ```text GET /api/schema/admin/mysql8/inventory/customers/stream?find={isActive:1}&sort=customer_id&select=first_name ``` ```json { "data": [ { "customer_id": 1, "first_name": "Bob" }, { "customer_id": 2, "first_name": "Alice" } ], "success": true, "statusCode": 200 } ``` - `data` comes first. Its rows go out in bunches while the query runs. - `success`, `statusCode` and `totalCount` close the answer. Read the whole answer as JSON, or read it as it comes with a streaming JSON parser. - `meta` closes it too, but only in the admin panel, with `x-am-meta: true`. See [the envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#the-envelope). - If the database fails after some rows went out, the answer still ends as valid JSON : `], "success": false, "statusCode": 500, "errors": [...] }`. So always read `success` at the end. ## Good to know - **Hooks.** Pre hooks run before the query, and can change it. Post hooks do not run on a stream. To change the rows, read them in a custom API with `g.sys.db.getAllByStream`, as below. - **Columns.** What the groups of the caller may not read is taken out of every bunch, as on get all. - **Headers.** The answer is JSON and is never compressed, whatever the headers say. `x-am-data-source` is always `api`. - **The caller leaves.** When the client closes the connection, the query stops at once. ## From your code In a custom API, the same call gives you each row as it comes. You never hold the whole list : ```kotlin var count = 0 g.sys.db.getAllByStream(GetAll("mysql8", "inventory", "customers").find(mapOf("isActive" to 1))) { row -> count++ // one object at a time, in order } return mapOf("count" to 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` | | From code | [`g.sys.db.gen.getAllByStreamGen`](https://docs.apimaker.dev/v4/examples/sys/db/getAllByStream.kotlin.md#without-a-schema) | A filter matches the value as you send it : `"22"` does not match the number `22`. On MongoDB, a text that looks like an ObjectId is turned into one. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # Get by ID API > Read one row by its primary key with the get by id API of API Maker, or by another column with the primaryKey part of the URL, with select and deep, and a pre hook to keep callers to their own rows. 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 another column. With `select` and `deep`, it fills the detail screen of your app in one call. > **In short** > > - `GET /api/schema////
/get-by-id/` > - Add a column after the id to look in that column : `/get-by-id/Bob/first_name`. > - It takes `select` and `deep`. No other query param. > - An unknown id answers `data: null`, with `success: true`. | | | |---|---| | 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.md), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) | | Answer | `data` : the row, or `null` | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, when `enableCaching` is on for the table or the database | | From code | [`g.sys.db.getById`](https://docs.apimaker.dev/v4/examples/sys/db/getById.kotlin.md) | | API id | `SCHEMA_GET_BY_ID` | | 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.md#without-a-schema) | The examples read the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## 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 `_id` on MongoDB, and the primary key column on SQL (`isPrimaryKey` in the schema). ## By another column Write the column after the id : ```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 comes back. - The id takes the type of the column : `382345` is compared as a number. A value that does not fit is refused : `Unable to convert 'abc' to number based on your schema type.` ## Columns 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.md#related-rows-with-deep). ## Only the rows of the caller Get by id reads no `find` from the URL. A [pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) can add one to `g.req.query.find` : the row must then match both the id and that `find`. That is how you keep each person to their own rows : see [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.kotlin.md). ## From your code [`g.sys.db.getById`](https://docs.apimaker.dev/v4/examples/sys/db/getById.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## 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` | | From code | [`g.sys.db.gen.getByIdGen`](https://docs.apimaker.dev/v4/examples/sys/db/getById.kotlin.md#without-a-schema) | Without a schema, the id takes the type it looks like : on MongoDB, an id that looks like an ObjectId is one, a number is a number, anything else is a text. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # Save Single or Multiple API > Insert one row or many with the save single or multiple API of API Maker - one object or a list in the body, ids made for you, what the schema checks, select or deep on the answer, or only the count with returnData=false. 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 (a list). It answers `201` with the saved rows, ids included. > **In short** > > - `POST /api/schema////
/save-single-or-multiple` > - The schema checks every row first. One error, and nothing is saved. > - `?returnData=false` answers only how many rows were saved : best for imports. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/save-single-or-multiple` | | Body | one object, or a list of objects | | Query params | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.md) and [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) shape the answer. `returnData=false` : the count only. | | Answer | `201`. `data` : the saved row, or the list of saved rows. With `returnData=false` : `{ savedRowsCount }`. | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no. It clears the cached answers of the table. | | From code | [`g.sys.db.saveSingleOrMultiple`](https://docs.apimaker.dev/v4/examples/sys/db/saveSingleOrMultiple.kotlin.md) | | API id | `SCHEMA_POST_BULK_INSERT` | | 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.md#without-a-schema) | - Video: [Save, Update and Remove on Any Database with the Same API Calls](https://apimaker.dev/videos/write-apis) (5 min) The examples write the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## 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 } } ``` Send the primary key (`"customer_id": 27`), or leave it out. Then the database or API Maker makes it : `isAutoIncrementByDB`, `isAutoIncrementByAM` or `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 a list, in the same order. - Rows with and without a primary key can mix. ## What the schema checks Before anything is written, the [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) works on every row : 1. A key the schema does not have is refused (`schemaKeyNotFound`). 2. Values take their types, texts are trimmed and cased, `conversionFun` runs. 3. Columns are encrypted or hashed, ids and defaults are filled. 4. The rules and `validatorFun` run. Every error comes back at once, with `400`, and nothing is saved. A `unique` error names its row with `dataIndex` : its place in the list, from `0`. ## A related row inside the row A column with a relation in the schema (`table` and `column`) takes a whole object. API Maker saves it to its own table first, then puts its id in the column. If a save fails, the rows saved so far are removed again. **Body : the state goes to the table states, then the city is saved with its id** ```json { "city_name": "Wembley", "state_id": { "state_name": "London", "country_id": 302 } } ``` To update related rows too, or to save lists of them, use [master save](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-master-save-api.md). ## The answer you want ```text POST /api/schema/admin/mysql8/inventory/customers/save-single-or-multiple?select=customer_id,first_name ``` `select` keeps some columns of the saved rows. `deep` adds their related rows. ## Only the count : returnData=false ```text POST /api/schema/admin/mysql8/inventory/customers/save-single-or-multiple?returnData=false ``` **Body** ```json [ { "first_name": "Bob", "last_name": "Lin" }, { "first_name": "Alice", "last_name": "Ray" } ] ``` **Answer** ```json { "success": true, "statusCode": 201, "data": { "savedRowsCount": 2 } } ``` - The saved rows are not read back. So the save costs the insert only : no extra query, no memory for the rows. - The schema checks the rows as always. - Without `returnData`, the answer is the saved rows. ## After the save The cached answers of the table are cleared. Subscribed WebSocket clients get the news, and the events of this API run. Post hooks get the answer in `g.res.output` : the saved rows, or `{ savedRowsCount }`. ## From your code [`g.sys.db.saveSingleOrMultiple`](https://docs.apimaker.dev/v4/examples/sys/db/saveSingleOrMultiple.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. Pass `returnData: false` next to `saveData` for the count only. ## 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` | | From code | [`g.sys.db.gen.saveSingleOrMultipleGen`](https://docs.apimaker.dev/v4/examples/sys/db/saveSingleOrMultiple.kotlin.md#without-a-schema) | The rows go to the database as you send them : nothing is converted, checked, filled, encrypted or numbered. On MongoDB, a text that looks like an ObjectId is turned into one. - On MongoDB, a nested object stays an embedded document. It is not saved to another collection : that needs the relations of a schema. - On an SQL table, every key must be a column. - On SQL, the answer is what the database says about the insert (`affectedRows`…). Without a schema, API Maker knows no column to read the rows back by. `returnData=false` answers `{ savedRowsCount }` here too. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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 Saves a whole tree of related rows in one call : an order with its customer and its items, a city with its state and country. Each row is inserted, or updated when its primary key is already there. > **In short** > > - `POST /api/schema////
/master-save` > - A row with the key of a row that is there updates that row. Any other row is inserted. > - The relations of the schemas say which table each nested object goes to. > - If one row fails, every row the call wrote is taken back. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/master-save` | | Body | one object, or a list of objects, with nested related objects | | Query params | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.md) and [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) shape the answer. `returnData=false` : the count only. | | Answer | `201`. `data` : the saved rows, with the ids of their related rows. With `returnData=false` : `{ savedRowsCount }`. | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no. It clears the cached answers of the table. | | From code | [`g.sys.db.masterSave`](https://docs.apimaker.dev/v4/examples/sys/db/masterSave.kotlin.md) | | API id | `SCHEMA_MASTER_SAVE` | | 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.md#without-a-schema) | - Video: [Master Save: Write a Tree of Rows Across Databases in One Call](https://apimaker.dev/videos/master-save) (4 min) ## Insert or update ```text POST /api/schema/admin/mysql8/inventory/customers/master-save ``` **Body : no primary key, so the row is inserted** ```json { "first_name": "Bob", "last_name": "Lin" } ``` **Body : customer 27 is there, so only its pincode changes** ```json { "customer_id": 27, "pincode": 382330 } ``` - The same rule holds at every level of the tree. - An update changes only the columns you send. - A key that matches no row is inserted, with that key. - A list in the body saves each object, and the answer is a list. ## A tree of rows Each nested object needs a relation in the schema of its column (`table`, `column`, and `database` or `instance` when they differ). The nested row is saved first, in its own table. Then its id goes into the column of the parent. **Body : three tables in 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, and its id goes into the state. Then the state is saved, and its id goes into the city. The tables can be in other 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. To get the related rows back in the answer, add `deep` : `?deep=[{s_key:'state_id',deep:[{s_key:'country_id'}]}]`. ## A list of child rows : virtual fields A [virtual field](https://docs.apimaker.dev/v4/docs/schema/schema.md#relations-and-virtual-fields) (`isVirtualField`, with `s_columnVirtualLinker` and `t_columnVirtualLinker`) takes a list. Each item is saved in the other 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 has its primary key, so it is updated. ## When something fails - Every row the call wrote is taken back when a later row fails. A wrong column in the country of the tree above leaves no city and no state behind. - The schema checks each row as [save single or multiple](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-save-single-or-multiple-api.md#what-the-schema-checks) does. The errors name the column. A `unique` error also names the row with `dataIndex`. ## Only the count : returnData=false `POST .../master-save?returnData=false` answers `{ "savedRowsCount": 2 }` : how many rows of the body were saved, nested rows not counted. The nested rows are saved and linked as always. ## Optimistic concurrency control A table whose schema has a version column (`isConcurrencyControlField`) needs the version in every update. An old version is refused with `400`. See [Optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.md). ## From your code [`g.sys.db.masterSave`](https://docs.apimaker.dev/v4/examples/sys/db/masterSave.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. Pass `returnData: false` next to `saveData` for the count only. ## 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` | | From code | [`g.sys.db.gen.masterSaveGen`](https://docs.apimaker.dev/v4/examples/sys/db/masterSave.kotlin.md#without-a-schema) | Without a schema, nothing says which rows belong to other tables. So master save works like [save single or multiple](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-save-single-or-multiple-api.md#without-a-schema) : every row is inserted as you send it, nothing is updated. On MongoDB, a nested object stays an embedded document. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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 the lists inside MongoDB documents : push, addToSet, pull, pullAll, pop and set. It works on every document that `find` matches, with several operations in one call. > **In short** > > - `PUT /api/schema/////array-operations` > - MongoDB only. An SQL table answers `400` : `This API is only supported for mongodb.` > - The body holds a `find` and a list of `operations`, run one after the other. | | | |---|---| | Method | PUT | | URL | `/api/schema/admin/mongodb/inventory/customers/array-operations` | | Body | `{ find, select?, operations: [ … ] }` | | Query params | none | | Answer | `data` : one list for each operation, with the matched documents after it | | Databases | MongoDB only | | Cached | no. It clears the cached answers of the collection. | | From code | [`g.sys.db.arrayOperations`](https://docs.apimaker.dev/v4/examples/sys/db/arrayOperations.kotlin.md) | | API id | `SCHEMA_ARRAY_OPERATIONS` | | Without a schema | `/api/gen/admin/mongodb/inventory/customers/array-operations` : [below](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-array-operations-api.md#without-a-schema) | ## The body The examples change this document of `customers` : ```json { "_id": "63e0775abd0e063920533f7c", "customer_id": 1, "ages": [ { "age": 33, "birth_year": 1989 } ], "tags": [ "new", "draft" ] } ``` **Body : push one item into ages** ```json { "find": { "customer_id": 1 }, "operations": [ { "operation": "push", "path": "ages", "dataToPush": [ { "age": 60, "birth_year": 1962 } ] } ] } ``` | Key | What it holds | |---|---| | `find` | Which documents change. `{}` : all of them. | | `select` | The fields to answer for the matched documents. Without it, the list of `path`. | | `operations` | The changes, run one after the other, each on every matched document. | | Operation | Keys | What it does | |---|---|---| | `push` | `path`, `dataToPush` (a list), `position`, `slice`, `sort` | Adds the items. `position` : at this index. `slice` : keep the first N items (the last N when negative). `sort` (`{ age: 1 }`) : reorder. | | `addToSet` | `path`, `dataToPush` (a value or a list) | Adds only the values that are not there yet. | | `pull` | `path`, `queryToRemove` | Removes every item that matches the query. | | `pullAll` | `path`, `dataToPull` (a list) | Removes every item equal to one of the values. | | `pop` | `path`, `direction` | `1` removes the last item, `-1` the first. | | `set` | `dataToSet`, `arrayFilters`, `upsert` | Changes fields of the items that `arrayFilters` pick, with `$[item]`. | Each operation refuses a missing key : `Please provide 'queryToRemove' to query array items and remove them.` ## Push and pull **Push two items at the front, sort by age, keep 10 items** ```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": [ "new", "draft" ] } ] } ``` ## Change items in place : set ```json { "find": {}, "operations": [ { "operation": "set", "dataToSet": { "ages.$[item].age": 45 }, "arrayFilters": [ { "item.birth_year": 1989 } ] } ] } ``` - `arrayFilters` is required : it says which items `$[item]` stands for. - `upsert: true` inserts a document when `find` matches none. ## Several operations at once ```json { "find": { "customer_id": 1 }, "operations": [ { "operation": "push", "path": "ages", "dataToPush": [ { "age": 60 } ] }, { "operation": "pull", "path": "ages", "queryToRemove": { "age": 33 } } ] } ``` **Answer : one list for each operation** ```json { "success": true, "statusCode": 200, "data": [ [ { "_id": "63e0775abd0e063920533f7c", "ages": [ { "age": 33, "birth_year": 1989 }, { "age": 60 } ] } ], [ { "_id": "63e0775abd0e063920533f7c", "ages": [ { "age": 60 } ] } ] ] } ``` ## Good to know - **With a schema**, the items that are pushed, added or set are converted and checked like a save, encrypted columns included. A `path` that is not a list in the schema is refused : `Type of 'ages' should be array in schema.` - **Columns.** The columns of `find` must be ones the groups of the caller may use. ## From your code [`g.sys.db.arrayOperations`](https://docs.apimaker.dev/v4/examples/sys/db/arrayOperations.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## Without a schema : /api/gen Every collection also answers this operation under `/api/gen`, with or without a schema : | | | |---|---| | URL | `/api/gen/admin/mongodb/inventory/customers/array-operations` | | API id | `GEN_ARRAY_OPERATIONS` | | From code | [`g.sys.db.gen.arrayOperationsGen`](https://docs.apimaker.dev/v4/examples/sys/db/arrayOperations.kotlin.md#without-a-schema) | The items go in as you send them : nothing is converted or checked. A text that looks like an ObjectId is turned into one. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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 columns you send on one row, and leaves the others as they are. The row is found by its primary key, or by the column you name. > **In short** > > - `PUT /api/schema////
/update-by-id/` > - The body holds only the columns that change. > - `?upsert=true` inserts the row when the id is not there. > - An unknown id changes nothing and answers `data: null`, unless you ask for a `404`. | | | |---|---| | Method | PUT | | URL | `/api/schema/admin/mysql8/inventory/customers/update-by-id/:id[/:primaryKey]` | | Body | the columns to change | | Query params | [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.md), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md), [upsert](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#upsert), [returnDocument](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#returnDocument), [throwErrorIfRecordNotFound](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#throwErrorIfRecordNotFound) | | Answer | `data` : the row after the change, or before it with `returnDocument=before` | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no. It clears the cached answers of the table. | | From code | [`g.sys.db.updateById`](https://docs.apimaker.dev/v4/examples/sys/db/updateById.kotlin.md) | | API id | `SCHEMA_PUT_UPDATE_BY_ID` | | 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.md#without-a-schema) | The examples change the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## Change some columns ```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 columns at once are fine. - On MongoDB, a key with dots changes that path only : `"address.city"`. ## Find the row by another column ```text PUT /api/schema/admin/mysql8/inventory/customers/update-by-id/Mallory/first_name ``` **Body** ```json { "first_name": "Alice" } ``` The row whose `first_name` is `Mallory` gets the new name. The column of the URL can be in the body too. ## More options | Option | What it does | |---|---| | `?upsert=true` | No row 999 ? One is inserted, with the id and the body. It is checked like a new row. See [upsert](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#upsert). | | `?returnDocument=before` | The answer holds the row as it was. The update happens all the same. | | `?throwErrorIfRecordNotFound=true` | An unknown id answers `404` : `Record not found.` | | `?select=first_name&deep=…` | Shapes the row of the answer. | ## What the schema does - The columns you send are converted and checked like a save : types, trim, case, `conversionFun`, encryption, rules, `validatorFun`. A key the schema does not have is refused. Every error comes back at once, with `400`. - `required` is checked for new rows only. An update does not check it. - A nested object on a column with a relation is saved in its own 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.md). - **Version check.** When the schema has a column with `isConcurrencyControlField`, the body must hold it, with the value the row has. Else the answer is `400` : `Concurrency version mismatch in 'version'. This row/document is already updated.` Nothing changes. See [Optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.md). ## Only the rows of the caller Update by id reads no `find` from the URL. A [pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) can add one to `g.req.query.find`. The row must then match both the id and that `find`, else nothing changes. ## From your code [`g.sys.db.updateById`](https://docs.apimaker.dev/v4/examples/sys/db/updateById.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## 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` | | From code | [`g.sys.db.gen.updateByIdGen`](https://docs.apimaker.dev/v4/examples/sys/db/updateById.kotlin.md#without-a-schema) | The columns go in as you send them : nothing is converted or checked. On MongoDB, a text that looks like an ObjectId is turned into one. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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, and the rows with returnData. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-many-api.html Makes the same change to every row that `find` matches, in one statement of the database. It answers how many rows changed. With `returnData: true`, it answers the rows too. > **In short** > > - `PUT /api/schema////
/update-many` > - Body : `{ "find": {…}, "updateData": {…} }`. `"find": {}` changes every row. > - The answer is `{ updatedRowsCount }`. > - No version check of optimistic concurrency control here. | | | |---|---| | Method | PUT | | URL | `/api/schema/admin/mysql8/inventory/customers/update-many` | | Body | `{ find, updateData, returnData? }` | | Query params | none | | Answer | `data` : `{ updatedRowsCount }`. With `returnData: true` : `{ updatedRowsCount, updatedRows }`. | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no. It clears the cached answers of the table. | | From code | [`g.sys.db.updateMany`](https://docs.apimaker.dev/v4/examples/sys/db/updateMany.kotlin.md) | | API id | `SCHEMA_UPDATE_MANY` | | 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.md#without-a-schema) | The examples change the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## 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.kotlin.md) : `$in`, `$gt`, `$and`, `$or`, `$like`, keys with dots… - `updatedRowsCount` counts the rows the database really changed. ## The changed rows : returnData **Body** ```json { "find": { "customer_id": { "$in": [ 2, 3 ] } }, "updateData": { "last_name": "Brown" }, "returnData": true } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "updatedRowsCount": 2, "updatedRows": [ { "customer_id": 2, "first_name": "Alice", "last_name": "Brown", "last_update": "2022-10-15T02:10:40.000Z", "pincode": 382346, "isActive": 1 }, { "customer_id": 3, "first_name": "Mallory", "last_name": "Brown", "last_update": "2022-09-13T03:44:05.000Z", "pincode": 382347, "isActive": 1 } ] } } ``` - API Maker reads the keys of the matching rows first, then the rows after the change. So a change to the very columns `find` looks at still finds them. - That costs two reads more than the update. Ask for the rows only when you use them. ## More filters **Two conditions : $and** ```json { "find": { "$and": [ { "isActive": 1 }, { "pincode": { "$gt": 382345 } } ] }, "updateData": { "isActive": 0 } } ``` **A related table : 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 checked like an update : types, `conversionFun`, encryption, rules. `find` is converted like a query. - The [version check](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.md) does not apply : the rows are changed in the database without being read first. - The columns of `updateData` must be ones the groups of the caller may write. ## Good to know - Without `returnData`, no row is read back. - The cached answers of the table are cleared, and WebSocket subscribers get the news once for the call. ## From your code [`g.sys.db.updateMany`](https://docs.apimaker.dev/v4/examples/sys/db/updateMany.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. Add `returnData: true` next to `find` and `updateData` for the changed rows. ## 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` | | From code | [`g.sys.db.gen.updateManyGen`](https://docs.apimaker.dev/v4/examples/sys/db/updateMany.kotlin.md#without-a-schema) | The values go in as you send them, and `find` matches the type you send. On MongoDB, a text that looks like an ObjectId is turned into one. On SQL without a schema, API Maker knows no primary key : with `returnData: true`, `updatedRows` are the rows that `find` matches after the update. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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. A field you do not send is gone. To change some fields only, use [update by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.md). > **In short** > > - `PUT /api/schema/////replace-by-id/` > - MongoDB only. An SQL table answers `400`. > - The body is the whole new document : the schema checks it like a new row. | | | |---|---| | 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.md), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md), [upsert](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#upsert), [returnDocument](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#returnDocument), [throwErrorIfRecordNotFound](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#throwErrorIfRecordNotFound) | | Answer | `data` : the document after the change, or before it with `returnDocument=before` | | Databases | MongoDB only | | Cached | no. It clears the cached answers of the collection. | | From code | [`g.sys.db.replaceById`](https://docs.apimaker.dev/v4/examples/sys/db/replaceById.kotlin.md) | | API id | `SCHEMA_PUT_REPLACE_BY_ID` | | 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.md#without-a-schema) | ## 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 that is not in the body disappears. - By another column : `/replace-by-id/4/customer_id`. ## Options | Option | What it does | |---|---| | `?upsert=true` | Inserts the document when the id is not there. | | `?returnDocument=before` | Answers the old document. | | `?throwErrorIfRecordNotFound=true` | Answers `404` for an unknown id, in place of `data: null`. | | `select`, `deep` | Shape the answer. | ## What the schema does The body is checked like a new row : conversions, defaults, made values and every rule apply. So every `required` field must be there. When the schema has a version field of [optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.md), it must match the document. ## On an SQL table The answer is `400` : `Replace by id API is not supported for instance type 'MYSQL_DB'.` Use [update by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.md) with every column instead. ## From your code [`g.sys.db.replaceById`](https://docs.apimaker.dev/v4/examples/sys/db/replaceById.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## Without a schema : /api/gen Every collection 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` | | From code | [`g.sys.db.gen.replaceByIdGen`](https://docs.apimaker.dev/v4/examples/sys/db/replaceById.kotlin.md#without-a-schema) | The document goes in as you send it : nothing is converted or checked. A text that looks like an ObjectId is turned into one. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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. The answer is the row as it was, so the caller can show or log what is gone. > **In short** > > - `DELETE /api/schema////
/` > - An unknown id deletes nothing and answers `data: null`, unless you ask for a `404`. > - To delete many rows, use [remove by query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-query-api.md). | | | |---|---| | 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.md), [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md), [throwErrorIfRecordNotFound](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#throwErrorIfRecordNotFound) | | Answer | `data` : the removed row, or `null` | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no. It clears the cached answers of the table. | | From code | [`g.sys.db.removeById`](https://docs.apimaker.dev/v4/examples/sys/db/removeById.kotlin.md) | | API id | `SCHEMA_DEL_DELETE_BY_ID` | | 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.md#without-a-schema) | The examples change the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## 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", "last_update": "2022-11-14T04:34:58.000Z", "pincode": 382345, "isActive": 1 } } ``` By another column : `DELETE /api/schema/admin/mysql8/inventory/customers/Bob/first_name` removes the first row whose `first_name` is `Bob`. ## Options | Option | What it does | |---|---| | `?throwErrorIfRecordNotFound=true` | An unknown id answers `404` : `Record not found.` | | `?select=first_name` | Keeps some columns of the removed row in the answer. | | `?deep=…` | Adds the related rows of the removed row to the answer. | ## Only the rows of the caller Remove by id reads no `find` from the URL. A [pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) can add one to `g.req.query.find`. The row is then removed only when it matches both the id and that `find`. ## After the call The cached answers of the table are cleared, and WebSocket subscribers get the news. Post hooks get the removed row in `g.res.output`. ## From your code [`g.sys.db.removeById`](https://docs.apimaker.dev/v4/examples/sys/db/removeById.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## 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` | | From code | [`g.sys.db.gen.removeByIdGen`](https://docs.apimaker.dev/v4/examples/sys/db/removeById.kotlin.md#without-a-schema) | Without a schema, the id takes the type it looks like. On MongoDB, an id that looks like an ObjectId is one. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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 a query that is too long or too private for a URL. It takes the keys of get all, in a JSON body. > **In short** > > - `POST /api/schema////
/query` > - Body : `{ "find": {…}, "sort": …, "skip": …, "limit": …, "select": …, "deep": […], "getTotalCount": true }` > - `find` is required. `{}` matches every row. > - Cached like get all, when caching is on. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/query` | | Body | `{ find, select, sort, skip, limit, deep, getTotalCount }` | | Query params | none | | Answer | `data` : a list of rows. `totalCount` with `getTotalCount: true`. | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, when `enableCaching` is on for the table or the database | | From code | [`g.sys.db.query`](https://docs.apimaker.dev/v4/examples/sys/db/query.kotlin.md) | | API id | `SCHEMA_POST_QUERY` | | 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.md#without-a-schema) | The examples read the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## The body | Key | What it holds | |---|---| | `find` | Required. The filter, with every operator of [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md). `{}` for every row. | | `select` | The columns : `"first_name,last_name"`, `"-password"`, or `{ "first_name": 1 }`. See [select](https://docs.apimaker.dev/v4/docs/apis-all/query-params/select.md). | | `sort` | `"-last_update,first_name"`, or `{ "last_update": -1 }`. | | `skip`, `limit` | Pages. See [skip and limit](https://docs.apimaker.dev/v4/docs/apis-all/query-params/query-params.md#skip). | | `deep` | Related rows. See [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md). | | `getTotalCount` | `true` adds `totalCount` : how many rows match `find`, before `skip` and `limit`. | Any other key is refused : `Only ['find', 'sort', 'select', 'skip', 'limit', 'deep', …] properties are allowed.` ```text POST /api/schema/admin/mysql8/inventory/customers/query ``` **Body** ```json { "find": { "isActive": 1, "pincode": { "$gte": 382346 } }, "select": "first_name,last_name", "sort": "-customer_id", "limit": 2, "getTotalCount": true } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": [ { "customer_id": 5, "first_name": "Eve", "last_name": "Page" }, { "customer_id": 4, "first_name": "Eve", "last_name": "Mathly" } ], "totalCount": 4 } ``` ## Filter on a related table A key with dots in `find` reaches a column of a related table, also in another database or instance : **The orders of gold customers : orders.customer_id refers to customers** ```json { "find": { "customer_id.loyalty_tier": "GOLD" } } ``` The relation comes from the schema. Every rule is on [find and join](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md#find-and-join-fields-of-related-tables). ## Related rows in the answer **Body : 5 customers, each with their orders, each order with its shipping** ```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" } ] } ] } ``` With relations in the schema, `{ "s_key": "customer_id" }` is enough. See [deep](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md). ## Good to know - **Types.** The values of `find` take the types of the schema : `"customer_id": "8"` matches the number `8`. - **Cache.** With caching on, the same body answers from Redis until a write changes the table. The hooks do not run for a cached answer. `x-am-cache-control: reset_cache` reads the database again. - **Hooks.** A pre hook can narrow `g.req.body.find`, for example to the rows of the person who calls. - **Many rows.** For a big result, use [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.kotlin.md). ## From your code [`g.sys.db.query`](https://docs.apimaker.dev/v4/examples/sys/db/query.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## 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` | | From code | [`g.sys.db.gen.queryGen`](https://docs.apimaker.dev/v4/examples/sys/db/query.kotlin.md#without-a-schema) | `find` matches the type you send : `"22"` does not match the number `22`. On MongoDB, a text that looks like an ObjectId is turned into one. A key with dots is not a join here : it goes to the database as it is. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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.md), 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.kotlin.md) : the rows go out as the database sends them. > **In short** > > - `POST /api/schema////
/query-stream` > - The body of the query API : `find` is required. > - The answer of get all by stream : read `success` at the end. > - Never cached. Pre hooks run, post hooks do not. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/query-stream` | | Body | `{ find, select, sort, skip, limit, deep, getTotalCount }` | | Query params | none | | Answer | `data` : a list of rows, sent as they come | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no : a stream always reads the database | | From code | [`g.sys.db.queryByStream`](https://docs.apimaker.dev/v4/examples/sys/db/queryByStream.kotlin.md) | | API id | `SCHEMA_POST_QUERY_STREAM` | | 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.kotlin.md#without-a-schema) | ## The call ```text POST /api/schema/admin/mysql8/inventory/customers/query-stream ``` **Body** ```json { "find": { "isActive": 1 }, "sort": "customer_id", "select": "first_name" } ``` **Answer, written in bunches** ```json { "data": [ { "customer_id": 1, "first_name": "Bob" }, { "customer_id": 2, "first_name": "Alice" } ], "success": true, "statusCode": 200 } ``` - The keys and operators of the query API work here, find and join and `deep` too. - An error after the first rows still closes the answer as JSON, with `"success": false` and the errors. - Everything else (hooks, columns, compression) is as on [get all by stream](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-by-stream-api.kotlin.md#good-to-know). ## From your code ```kotlin val rows = mutableListOf() g.sys.db.queryByStream( Query("mysql8", "inventory", "customers") .find(mapOf("isActive" to 1)).select("customer_id,first_name"), ) { row -> rows.add(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` | | From code | [`g.sys.db.gen.queryByStreamGen`](https://docs.apimaker.dev/v4/examples/sys/db/queryByStream.kotlin.md#without-a-schema) | `find` matches the type you send. On MongoDB, a text that looks like an ObjectId is turned into one. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.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 count of removed rows in the answer, and the rows with returnData. Source: https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-remove-by-query-api.html Deletes every row that `find` matches, and answers how many rows went. With `returnData: true`, it answers the removed rows and their ids too. > **In short** > > - `POST /api/schema////
/query/delete` > - Body : `{ "find": {…} }`. The answer is `{ deletedRowsCount }`. > - A missing or empty `find` is refused. So no call deletes a whole table by mistake. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/query/delete` | | Body | `{ find, returnData?, select?, deep? }` | | Query params | none | | Answer | `data` : `{ deletedRowsCount }`. With `returnData: true` : `{ deletedRows, deletedRowsCount, ids }`. | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | no. It clears the cached answers of the table. | | From code | [`g.sys.db.removeByQuery`](https://docs.apimaker.dev/v4/examples/sys/db/removeByQuery.kotlin.md) | | API id | `SCHEMA_POST_QUERY_DELETE` | | 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.md#without-a-schema) | The examples change the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## The call ```text POST /api/schema/admin/mysql8/inventory/customers/query/delete ``` **Body** ```json { "find": { "customer_id": { "$in": [ 4, 5 ] } } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "deletedRowsCount": 2 } } ``` - `find` takes every operator of [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md), keys of related tables included. - The rows are not read : the answer only says how many went. Removing a million rows costs the delete, nothing more. ## The removed rows : returnData **Body** ```json { "find": { "customer_id": 5 }, "returnData": true } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "deletedRows": [ { "customer_id": 5, "first_name": "Eve", "last_name": "Page", "last_update": "2022-11-12T01:59:33.000Z", "pincode": 382349, "isActive": 1 } ], "deletedRowsCount": 1, "ids": [ 5 ] } } ``` - The rows are read before the delete, so `deletedRows` holds them as they were. `select` keeps some of their columns. `deep` adds their related rows. - Every matching row is held in the memory of the server, or of the sandbox for code. So ask for the rows only when you use them. ## More filters **Two conditions** ```json { "find": { "$and": [ { "isActive": 0 }, { "last_update": { "$lt": "2022-01-01T00:00:00.000Z" } } ] } } ``` **A related table : the orders of one customer (orders.customer_id refers to customers)** ```json { "find": { "customer_id.email": "gone@example.com" } } ``` ## No find, no delete A body without `find`, or with `"find": {}`, deletes nothing. The answer is `500` : ```json { "success": false, "statusCode": 500, "errors": [ { "message": "Please provide 'find' property having query to remove data.", "field": null, "type": null, "value": { "find": {} } } ] } ``` To empty a table on purpose, use a filter that every row matches, like `{ "customer_id": { "$gte": 0 } }`. ## After the call The cached answers of the table are cleared, and WebSocket subscribers get the news once. Post hooks get the answer in `g.res.output`. It holds the rows only with `returnData: true`. ## From your code [`g.sys.db.removeByQuery`](https://docs.apimaker.dev/v4/examples/sys/db/removeByQuery.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. It returns `{ deletedRowsCount }`. Add `returnData: true` next to `find` for the rows. ## 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` | | From code | [`g.sys.db.gen.removeByQueryGen`](https://docs.apimaker.dev/v4/examples/sys/db/removeByQuery.kotlin.md#without-a-schema) | `find` matches the type you send. On MongoDB, a text that looks like an ObjectId is turned into one. On SQL without a schema, API Maker knows no primary key : with `returnData: true`, `ids` holds the removed rows themselves. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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 list of stages. The answer is what the pipeline makes. > **In short** > > - `POST /api/schema/////aggregate` > - MongoDB only. An SQL table answers `400` : `This API is only supported for mongodb.` > - The column permissions of the groups do not apply to its answer. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mongodb/inventory/customers/aggregate` | | Body | the pipeline : a list of stages | | Query params | none | | Answer | `data` : the documents the pipeline makes | | Databases | MongoDB only | | Cached | yes, when `enableCaching` is on for the collection or the database | | From code | [`g.sys.db.aggregate`](https://docs.apimaker.dev/v4/examples/sys/db/aggregate.kotlin.md) | | API id | `SCHEMA_POST_AGGREGATE` | | 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.md#without-a-schema) | ## The call ```text POST /api/schema/admin/mongodb/inventory/orders/aggregate ``` **Body : the paid orders of each customer, the biggest total 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 that is not a list is refused : `Please provide array for aggregate body.` ## Stages Every stage of your version of MongoDB works. 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": {} } }` | ## What API Maker adds - **Values in `$match`** are taken for what they look like : a text that looks like an ObjectId becomes one, an ISO date becomes a date. They do not take the types of the schema. - **Related tables.** On the schema API, the first `$match` can filter on a column of a related table, as a [find and join](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md#find-and-join-fields-of-related-tables) : `{ "$match": { "customer_id.loyalty_tier": "GOLD" } }`. - **`$sort` as a text** works too : `{ "$sort": "-total,_id" }`. - Big pipelines may use the disk of MongoDB (`allowDiskUse`). ## Good to know - **Permissions.** The groups decide who may call this API. But the column permissions of a group do not apply to what a pipeline answers : a `$project` or a `$lookup` can show any field. Give this API only to groups that may read every field. - **SQL.** For a report on an SQL database, run SQL with [executeQuery](https://docs.apimaker.dev/v4/examples/sys/system/executeQuery.kotlin.md) from a custom API. - **Cache.** 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.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## Without a schema : /api/gen Every collection 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` | | From code | [`g.sys.db.gen.aggregateGen`](https://docs.apimaker.dev/v4/examples/sys/db/aggregate.kotlin.md#without-a-schema) | The same pipeline, without find and join : a key with dots goes to MongoDB as it is. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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. > **In short** > > - `POST /api/schema////
/count` > - Body : `{ "find": {…} }`. `{ "find": {} }` counts the whole table. > - `data` is the number. | | | |---|---| | 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, when `enableCaching` is on for the table or the database | | From code | [`g.sys.db.count`](https://docs.apimaker.dev/v4/examples/sys/db/count.kotlin.md) | | API id | `SCHEMA_POST_COUNT` | | 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.md#without-a-schema) | The examples count the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## The call ```text POST /api/schema/admin/mysql8/inventory/customers/count ``` **Body** ```json { "find": { "pincode": { "$gte": 382347 } } } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": 3 } ``` `find` takes every operator of [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md), and the keys of related tables : **The orders of gold customers : orders.customer_id refers to customers** ```json { "find": { "customer_id.loyalty_tier": "GOLD" } } ``` ## Good to know - **Types.** The values take the types of the schema : `"pincode": "382345"` counts the number 382345. - **No related row.** When a filter on a related table matches no row there, `data` is an empty list `[]`, not `0`. - **A list and 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.md) with `getTotalCount: true`. - **Cache.** 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.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## 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` | | From code | [`g.sys.db.gen.countGen`](https://docs.apimaker.dev/v4/examples/sys/db/count.kotlin.md#without-a-schema) | `find` matches the type you send : `"22"` does not match the number `22`. On MongoDB, a text that looks like an ObjectId is turned into one. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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 different values of a column, or the different combinations of several columns, in the order you ask. > **In short** > > - `GET /api/schema////
/distinct/[/]` > - Several columns : `distinct/first_name,pincode`. > - Filter with the query params of get all : `?find={isActive:1}`. | | | |---|---| | 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.kotlin.md), [a column](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md#a-column-as-a-query-param) | | Answer | `data` : a list of objects, one for each value or combination | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, when `enableCaching` is on for the table or the database | | From code | [`g.sys.db.distinct`](https://docs.apimaker.dev/v4/examples/sys/db/distinct.kotlin.md) | | API id | `SCHEMA_GET_DISTINCT` | | 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.md#without-a-schema) | The examples read the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## One column ```text GET /api/schema/admin/mysql8/inventory/customers/distinct/first_name ``` ```json { "success": true, "statusCode": 200, "data": [ { "first_name": "Alice" }, { "first_name": "Bob" }, { "first_name": "Eve" }, { "first_name": "Mallory" } ] } ``` The order is ascending. For descending, add `desc` : `distinct/first_name/desc`. `-1`, `dsc`, `0` and `false` mean descending too. Anything else is ascending. ## Several columns ```text 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 }, { "first_name": "Mallory", "pincode": 382347 } ] } ``` One order for each column, in the same place. A missing order is ascending. ## With a filter ```text GET /api/schema/admin/mysql8/inventory/customers/distinct/pincode?find={first_name:'Eve'} ``` To send the filter in a body, use [distinct with query](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-distinct-with-query-api.md). ## Good to know - No column : `Please provide field for distinct API.` An unknown column : `Invalid column ['nick_name'] found in your field list.` - A column the groups of the caller may not read can not be listed. - When no row has a value in the column, `data` is `[]`. ## From your code [`g.sys.db.distinct`](https://docs.apimaker.dev/v4/examples/sys/db/distinct.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## 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` | | From code | [`g.sys.db.gen.distinctGen`](https://docs.apimaker.dev/v4/examples/sys/db/distinct.kotlin.md#without-a-schema) | `find` matches the type you send. On MongoDB, a text that looks like an ObjectId is turned into one. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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.md), with the filter in a JSON body : the different values of a column, among the rows that `find` matches. > **In short** > > - `POST /api/schema////
/distinct/[/]` > - Body : `{ "find": {…} }`. `{ "find": {} }` lists every value. > - Columns and orders in the URL work as in distinct. | | | |---|---| | Method | POST | | URL | `/api/schema/admin/mysql8/inventory/customers/distinct/:field[/:order]` | | Body | `{ find }` | | Query params | none | | Answer | `data` : a list of objects, one for each value or combination | | Databases | MongoDB, MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB, Percona | | Cached | yes, when `enableCaching` is on for the table or the database | | From code | [`g.sys.db.distinctQuery`](https://docs.apimaker.dev/v4/examples/sys/db/distinctQuery.kotlin.md) | | API id | `SCHEMA_POST_DISTINCT_QUERY` | | 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.md#without-a-schema) | The examples read the table of [get all](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#the-url). ## 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 } ] } ``` - `find` takes every operator of [find](https://docs.apimaker.dev/v4/docs/apis-all/query-params/find.kotlin.md) : `{ "first_name": { "$like": "%llory" } }`. - The values take the types of the schema : `"pincode": "382345"` matches the number. - The columns of `find` must be ones the groups of the caller may read. ## From your code [`g.sys.db.distinctQuery`](https://docs.apimaker.dev/v4/examples/sys/db/distinctQuery.kotlin.md) makes the same call from custom APIs, hooks, events, schedulers and test cases. ## 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` | | From code | [`g.sys.db.gen.distinctQueryGen`](https://docs.apimaker.dev/v4/examples/sys/db/distinctQuery.kotlin.md#without-a-schema) | `find` matches the type you send. On MongoDB, a text that looks like an ObjectId is turned into one. ## Headers and errors This operation takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) of every API. It answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md), with the [status codes](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md#status-codes) and the [messages](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) of every API. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) · [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) · [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) --- # 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 your own function behind a path and a method. Write it in any of the [languages](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) of API Maker. Save it in the admin panel (**APIs & Logic → Custom API**), or in your editor through the local client. It answers at `/api/custom-api//` at once. Your code reaches your databases and the rest of API Maker through the [global object `g`](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md). > **In short** > > - Two files : the **basic info** (path, method, access, cache…) in TypeScript, and the **code** in your language. > - `main` gets `g`. What it returns is `data` in the answer. > - The code runs in a sandbox of your account, or on the server with `runOnNativeProcess: true`. > **Tip : New to your own code ?** > > Start with [Your own code](https://docs.apimaker.dev/v4/docs/getting-started/first-custom-api.md) : one custom API, step by step, on the real screens. | | | |---|---| | 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.kotlin.md), 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.kotlin.md) in a test | - Video: [Your First Custom API: From a Function to a Live Endpoint](https://apimaker.dev/videos/first-custom-api) (6 min) ## 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.kotlin.md) ; 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; ``` **Code** ```kotlin import am.G fun main(g: G): Any? { return mapOf("hello" to g.req.query.getString("name", "world")) } ``` ```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.kotlin.md#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.md) 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.kotlin.md) 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.md#encrypted-request-payloads). | | `reqBodySchema`, `reqQueryParametersSchema` | A [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) 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.kotlin.md) 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 Sandbox Settings, 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.md#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.md) 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.kotlin.md) system API runs the same check without calling the API. ## Reach your data and the rest of API Maker ```kotlin import am.DeepItem import am.G import am.Json import am.Query fun main(g: G): Any? { val person = g.req.auth.getObject("authAMDB") // the signed-in person val orders = g.sys.db.query( Query("mongodb", "shop", "orders") .find(mapOf("customer_id" to person["customer_id"], "status" to "PAID")) .sort("-created_at").limit(10) .deep(listOf(DeepItem("items"))), ) val total = orders.sumOf { it.getDouble("amount") } g.logger.log("${orders.size} orders") // reaches the log table and the caller's logs g.sys.cache.setKey("orders:${person["customer_id"]}", Json.stringify(orders), 60) return mapOf("orders" to orders, "total" to total) } ``` - 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.kotlin.md) 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.kotlin.md). - Every method of `g` has a page in the [code examples](https://docs.apimaker.dev/v4/examples/index.kotlin.md). ## Errors and status codes ```kotlin import am.AmErrors import am.G fun main(g: G): Any? { if (!g.req.body.has("customer_id")) throw AmErrors("Please provide customer_id.", 400) // 400 with the message in errors if (g.req.body.getDouble("customer_id") < 0) { g.res.statusCode = 400 // 400 with your data return mapOf("field" to "customer_id", "message" to "A customer_id is positive.") } return mapOf("ok" to true) } ``` - An error your code raises answers `success: false`, with its message in `errors` : `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. - The `code` of the first error is the status of the answer when it is `400`, `401`, `403`, `404` or `500`. Any other code (`422`…) answers `500`, and the error keeps its code. For another status, set `g.res.statusCode`. - Any other exception answers `500` with its message. A message listed in `errorList` can be translated with 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 `files50`, to keep groups of files apart). They are on disk when the code runs, in `g.req.body.files` : ```kotlin import am.G fun main(g: G): Any? { // each : originalname, mimetype, size, path, filename return g.req.body.getObjects("files").map { mapOf("name" to it.getString("originalname"), "bytes" to it.getLong("size")) } } ``` - `path` is where the code finds the file wherever it runs : `/opt/am/work/uploads/` in the sandbox, the uploads folder of the server with `runOnNativeProcess`. `uploads/` from the folder the code works in is the same file. - A form may be as long as `uploadLimit` (512 MiB by default) : its files go to disk while they arrive, never into memory. Its text fields are in `g.req.body`, together no longer than `bodyLimit`. - `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). A request refused before the code runs (no such API, a caller who may not call it) leaves no file. **Download.** Return an object with the `__am__` keys of `IDownloadResponse` : the file (or a zip of several) is streamed to the caller. ```kotlin import am.G fun main(g: G): Any? { // a file written earlier in the uploads folder of the sandbox return mapOf( "__am__downloadFilePath" to "reports/2026-09.pdf", "__am__downloadFolderFileName" to "September report.pdf", "__am__cleanupFileOrFolderPaths" to "reports/2026-09.pdf", ) } ``` - `__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. ```kotlin import am.G fun main(g: G): Any? { g.res.contentType = "text/html" return "

Hello

" } ``` **A tiny gif** ```kotlin import am.G fun main(g: G): Any? { g.res.contentType = "image/gif" // another type : what is returned is base64 return "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=" } ``` - `g.res.headers` adds response headers. See [Content types](https://docs.apimaker.dev/v4/examples/res/res.kotlin.md#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.kotlin.md). - **Events and WebSockets** : an event can trigger automatically after a successful call of 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.kotlin.md#testing-a-custom-api) runs the code with mocks and measures its coverage. - **Git** : `src/custom_apis//` holds the item (`.yaml`), the basic info (`.config.ts`), the code (`.ts`, or `.py`, `.go`… in another language), its hooks and the docs. Every file of a custom API, and of every other kind of item, with what goes in it : [Items as files](https://docs.apimaker.dev/v4/docs/ai/items-as-files.kotlin.md#custom-api). - **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.md). ## 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 with the same packages : no sandbox start, the [packages of the Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#packages) of its language (installed on the machine the first time, in a folder of their own), 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.md). Code of a [group of separate sandboxes](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.md#separate-sandbox) runs on the runtime of its group, with the packages of its group. It runs on the version of its language you installed for it in [Sandbox settings → Native languages](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#native-languages) (every server installs it from the publisher of the language, for your account), or on the runtime of the server (`node`, `python3`, `php`…) in a [version API Maker runs](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md#versions), which [API Maker Launchpad](https://docs.apimaker.dev/v4/docs/getting-started/local-run.md#languages-for-native-apis) can install too. The code is not isolated from the server, so keep it for the few APIs which need it. - Video: [Separate Sandboxes for Heavy APIs: Bun, Sandbox Count and Restarts](https://apimaker.dev/videos/separate-sandboxes) (9 min) 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.kotlin.md#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.md#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. Open them on the Custom API page, or read each one in your language on [Custom API examples](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#samples) : | Sample | Shows | |---|---| | Login | [the token of the app and the token of the person](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#sign-in), one message for every failure | | Data Isolation | [only the rows of the person who calls](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#rows-of-the-caller), with inactive row scoping hooks on the instance, the database and the table `orders` | | Deep Populate Orders, Deep Populate Order Items | [rows with the rows they point to](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#deep-populate), on one and two levels | | Find Join Orders, Find Join Products | [a filter on a column of another table](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#find-join) | | Caching Example | [an answer kept until its tables change](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#cache-the-answer) | | Get Captcha, Login With Captcha | [a value kept for a while, used once](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#captcha) | | WebSocket Notify | [a message to one person over WebSocket](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#websocket-notify) | | Collection Copy Stream, Database Copy Stream | [rows read one by one](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#stream-to-a-file) | | S3 Upload, S3 Download, S3 List, Image Upload | [files in S3](https://docs.apimaker.dev/v4/examples/custom-apis/custom-api.kotlin.md#s3) | - Video: [Custom APIs on One Standard: Validation, Standard Errors, Fast Fixes](https://apimaker.dev/videos/custom-api-standards) (10 min) - Video: [Custom API Gems: Uploads, Downloads, npm Packages, a Browser, Caching](https://apimaker.dev/videos/custom-api-gems) (8 min) - Video: [One API in 8 Languages: TypeScript, Python, PHP, C#, Go, Rust, Java, Kotlin](https://apimaker.dev/videos/eight-languages) (5 min) --- # 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. It says 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.kotlin.md#basic-info) the code is written in : API Maker [runs it itself](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md#settings-run) when you save. So the tab shows TypeScript with a lock. > **In short** > > - `path` and `requestMethod` say where the API answers. Together they are unique in the account. > - `apiAccessType` and `authProviders` say who may call it. > - The other keys are optional : cache, request schemas, timeout, files, a sandbox of its own. | | | |---|---| | 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`. The path must match exactly : it holds no variables. Send values in the query or the body. | | `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.kotlin.md). | | `acceptOnlyEncryptedData` | Refuse plain payloads : [encrypted payloads](https://docs.apimaker.dev/v4/docs/features/security-features.md#encrypted-request-payloads). | | `reqBodySchema`, `reqQueryParametersSchema` | A [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) 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.kotlin.md). | | `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.kotlin.md#x-am-sandbox-timeout) is given that time instead : [time of a run](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#time-of-a-run). | | `runOnNativeProcess` | Run on the machine of the server instead of the sandbox : faster, less isolated ; `console.log` is not captured, use `g.logger`. The code finds the packages of the [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#packages) of its language (of its [group](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.md#separate-sandbox), when it names one) : they are installed on the machine the first time it runs. | | `errorList` | The messages this API throws, listed for [internationalization](https://docs.apimaker.dev/v4/docs/i18/i18.kotlin.md). | | `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.kotlin.md#files). | | `swaggerDocs` | `tag` and `parameters` for the Swagger docs of the API users. `parameters` are written as in OpenAPI 3, or the way of Swagger 2 : `{ in: 'formData', name: 'files', type: 'file' }` becomes a field of a form. | | `separateSandboxSettings` | Sandboxes of their own for this API : see [below](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.md#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.kotlin.md). ## 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.md). 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. - Video: [Separate Sandboxes for Heavy APIs: Bun, Sandbox Count and Restarts](https://apimaker.dev/videos/separate-sandboxes) (9 min) **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, autoRestart: { afterTheseMuchSeconds: 3600 }, // a new sandbox every hour, no request dropped 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, and the keys below say nothing. | | `runtime`, `version` | A runtime of the language of the code (`node` or `bun` for TypeScript) and a tag of its image. The runtime wins over the one the code names. Absent : the ones of the Sandbox Settings, with the version and Dockerfile they keep for that very runtime (a group on Bun never takes the version of Node.js). | | `sandboxCount` | The most sandboxes of the group at a time, on each server : 1 to 50. | | `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.md#dockerfile) of the group, with the same markers. | | `autoRestart.afterTheseMuchSeconds` | A sandbox of the group gives its place to a new one after this many seconds (60 at least) : the new one starts while the old one still takes the requests, then the old one finishes what it runs. No request fails for it. Empty or `0` : never. The [restart](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#restart-every) of the Sandbox Settings is not the group's. | - **The custom APIs of a group share it.** Every API of the account which names the same group runs in the same sandboxes : all their packages are there, and what one leaves in `g.shared` the others find. So the group is what they say together : its runtime, version, Dockerfile, sandbox count and restart are said once, in any of them, and an API which names another value than another API of the group is refused when it is saved : `The custom API 'monthly' of the group 'reports' names the version '1.3' : …`. Name it in one of them only, or the same in each. - **The hooks of the versions of the API** run in the sandboxes of its group too, in the same request. - 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. - Settings which can not be made are refused when the API is saved, in words : a runtime of another language, a version too old for its runtime, a package its registry does not take, a Dockerfile with `CMD`, a sandbox count out of range. So is a `runtime`, `version` or `sandboxCount` without a group : it would change nothing. - With [`runOnNativeProcess`](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md#native-process) the code runs on the machine of the server : on the runtime of its group, with the packages of its group. The version, Dockerfile, sandbox count and restart are the ones of sandboxes : it has none. - [API Testing](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#api-testing-page) of the admin panel, sending [`x-am-meta: true`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-meta), is told the group in `runBy` (`enableSeparateSandboxForThis`, `runtime`, `port`) ; [`x-am-run-in-sandbox`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-run-in-sandbox) picks among the oldest sandboxes of the group. ## Related - [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md) · [Automatic caching](https://docs.apimaker.dev/v4/docs/features/automatic-caching.md) · [Auth of a database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.md) --- # 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 your function that runs **before** an API, written in any of the [languages](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) of API Maker. It sees the request. It can change it, answer in place of the API, or refuse it. Attach it to an instance, a database, a table, one API, a custom API or a system API. Change it any time : nothing restarts. > **In short** > > - A pre hook is your code that runs **before** an API. > - It can change the request (add a filter, a limit), answer instead of the API, or refuse the call. > - Attach it to an instance, a database, a table or one API : it runs for every API below. > - Its most common job : keep each person to their own rows. [![A pre hook of the database sales, in its editor](https://docs.apimaker.dev/assets/images/screens/ref-hook.webp)](https://docs.apimaker.dev/assets/images/screens/ref-hook.webp) 1. The hook changes the request before the API runs : here, at most 50 rows. > 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. - Video: [API Hooks: Run Your Code Before and After Every API](https://apimaker.dev/videos/api-hooks) (7 min) ## 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.kotlin.md). - 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.md#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.kotlin.md#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 ```kotlin import am.G fun main(g: G): Any? { if (!g.req.isApiRequestFromUser) return null // calls made by your own code : leave them alone // g.req.query, g.req.body, g.req.params, g.req.headers : the request, changeable // g.req.auth.getObject("authAMUser"), g.req.auth.getObject("authAMDB")… : who calls // g.res.shared : values for the API and the post hooks of the same request return null } ``` What the return value does : | The hook | Effect | |---|---| | returns nothing | The next hook, then the API, run. | | returns a value | That value is the answer of the request, in `data` : the remaining hooks and the API **do not run**. | | sets `g.res.output` and returns nothing | The API runs ; its output replaces `g.res.output` unless a post hook changes it again. | | throws | The request stops with the error. An exception gives `500` and its message. An error of API Maker gives its own `code` as the status when it is `400`, `401`, `403`, `404` or `500`, else `500` : `throw [{ code: 400, message: '…' }]` in TypeScript, `raise Errors('…', code=400)` in Python, `AmErrors` in PHP, C#, Java and Kotlin, `am.Fail(400, '…')` in Go, `am::fail(400, '…')` in Rust. | - A hook in any language other than TypeScript runs in the sandbox of its language. A hook in TypeScript runs [inside API Maker](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md#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.md), 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.md) 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. **Collection level pre hook on orders** ```kotlin // Row scoping pre-hook for orders, as the API Security Report writes it. // Every request is limited to the rows whose "customer_id" equals the id of the person behind it // (g.req.auth.authAMDB.customer_id). Edit freely : the report reads the code again on every scan. import am.AmErrors import am.G const val OWNER_COLUMN = "customer_id" const val IDENTITY_FIELD = "customer_id" // Where each generated API keeps its filter. API Maker ANDs query.find onto the primary key for the *-by-id APIs. val QUERY_FIND_APIS = setOf( "SCHEMA_GET_ALL", "SCHEMA_GET_ALL_STREAM", "SCHEMA_GET_BY_ID", "SCHEMA_PUT_UPDATE_BY_ID", "SCHEMA_PUT_REPLACE_BY_ID", "SCHEMA_DEL_DELETE_BY_ID", "GEN_GET_ALL", "GEN_GET_ALL_STREAM", "GEN_GET_BY_ID", "GEN_PUT_UPDATE_BY_ID", "GEN_PUT_REPLACE_BY_ID", "GEN_DEL_DELETE_BY_ID", ) val BODY_FIND_APIS = setOf( "SCHEMA_POST_QUERY", "SCHEMA_POST_QUERY_STREAM", "SCHEMA_POST_QUERY_DELETE", "SCHEMA_UPDATE_MANY", "SCHEMA_POST_DISTINCT_QUERY", "SCHEMA_ARRAY_OPERATIONS", "SCHEMA_POST_COUNT", "GEN_POST_QUERY", "GEN_POST_QUERY_STREAM", "GEN_POST_QUERY_DELETE", "GEN_UPDATE_MANY", "GEN_POST_DISTINCT_QUERY", "GEN_ARRAY_OPERATIONS", "GEN_POST_COUNT", ) val STAMP_OWNER_APIS = setOf( "SCHEMA_POST_BULK_INSERT", "SCHEMA_MASTER_SAVE", "GEN_POST_BULK_INSERT", "GEN_MASTER_SAVE", ) @Suppress("UNCHECKED_CAST") fun main(g: G): Any? { // Custom APIs, schedulers and events calling g.sys.db run under their own rules, only real callers are scoped. if (!g.req.isApiRequestFromUser) return null val me = g.req.auth.getObject("authAMDB")[IDENTITY_FIELD] if (me == null || me == "") throw AmErrors("A person token is required for this API.", 401) val scope = mapOf(OWNER_COLUMN to me) // MERGE, never replace : $and keeps what the caller asked for and adds the owner on top, so a caller // can never widen the rows by sending its own owner filter. fun merge(existing: Any?): Any = if (existing is Map<*, *> && existing.isNotEmpty()) mapOf("\$and" to listOf(existing, scope)) else scope.toMutableMap() val apiId = g.req.reqInfo.getObject("apiInfo").getString("id", "") if (apiId in QUERY_FIND_APIS) { val query = g.req.objectAt("query") query["find"] = merge(query["find"]) val body = g.req["body"] if (body is MutableMap<*, *>) (body as MutableMap)[OWNER_COLUMN] = me // update-by-id and replace-by-id : the owner of a row can not be handed over return null } if (apiId in BODY_FIND_APIS) { val body = g.req.objectAt("body") body["find"] = merge(body["find"]) val updateData = body["updateData"] if (updateData is MutableMap<*, *>) (updateData as MutableMap)[OWNER_COLUMN] = me // update-many return null } if (apiId in STAMP_OWNER_APIS) { val rows = g.req["body"] as? List<*> ?: listOf(g.req.objectAt("body")) for (row in rows) { if (row is MutableMap<*, *>) (row as MutableMap)[OWNER_COLUMN] = me // never trust the owner sent by the caller } return null } // Fail closed : aggregate and distinct can not be scoped generically, so they are refused for row scoped callers. throw AmErrors("API $apiId is not available for row scoped users.", 403) } ``` The report writes this hook for you, for the tables which need it, and reads it again on every scan. [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.kotlin.md) builds the same pattern for several tables at once, with a group which reads every row. ## Answer without the API **A pre hook which answers from the cache** ```kotlin import am.G import am.Json fun main(g: G): Any? { val cached = g.sys.cache.getKey("countries") return cached?.let { Json.parse(it) } // the API does not run } ``` ## Validate and change ```kotlin import am.AmErrors import am.G fun main(g: G): Any? { if (!g.req.isApiRequestFromUser) return null if (g.req.body.getDouble("price_cents") < 0) throw AmErrors("A price can not be negative.", 400) g.req.body["updated_by"] = g.req.auth.getObject("authAMDB")["email"] g.req.headers["x-am-response-case"] = "camelCase" // headers can change too g.res.shared["startedAt"] = System.currentTimeMillis() // for the post hook of this request return null } ``` ## Utility classes in a hook A hook calls your [utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.kotlin.md) like any other code of its language : a check shared by many hooks lives in one. **The utility class Guards** ```kotlin import am.AmErrors import am.G /** The person of the request has the role, or the request is refused. */ object Guards { fun requireRole(g: G, role: String) { if (role !in g.req.auth.getObject("authAMDB").getStrings("roles")) throw AmErrors("The role $role is needed.", 403) } } ``` **A pre hook which uses it** ```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.kotlin.md). ## Testing a hook - The API testing page runs an API with its hooks. While you edit a hook there, the **Test** button of the hook runs only that hook, so you see its effect alone. - A level holds a list of hooks, each one active or not. Keep the old hook active until the new one is ready, then switch. - The log profile records each hook run with the request ; the log explorer shows them next to the API call. ## Good to know - Streams : pre hooks run before the stream starts. Post hooks do not run on a stream. - A hook which calls the same API it is attached to creates a cycle : API Maker detects it and stops the request with an error. - Hooks are files in Git, under the folder of their instance, database, table or API, and deploy with a Git pull. - [Post hooks →](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.kotlin.md) - [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.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 your function that runs **after** an API, with the result in `g.res.output`. Write it in any of the [languages](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) of API Maker. It can reshape the answer, add to it, replace it, send something elsewhere, or refuse to answer. It lives at the same levels as a [pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) : instance, database, table, API, custom and system API, with optional groups. > **In short** > > - A post hook is your code that runs **after** an API, with its answer in `g.res.output`. > - It can change the answer, add to it, or send something elsewhere. > - It lives at the same levels as a [pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) : instance, database, table, one API. > 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. - Video: [API Hooks: Run Your Code Before and After Every API](https://apimaker.dev/videos/api-hooks) (7 min) ## 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.md#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.kotlin.md#one-language). ## The code ```kotlin import am.G fun main(g: G): Any? { if (!g.req.isApiRequestFromUser) return null // 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 return null } ``` | 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.kotlin.md#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.md). Calls from your own code skip hooks unless `skipHookRunning: false`. ## Reshape the answer **Hide a field and add a computed one** ```kotlin import am.G fun main(g: G): Any? { if (!g.req.isApiRequestFromUser) return null val rows = g.res.output as? List<*> ?: listOf(g.res.output) for (each in rows) { @Suppress("UNCHECKED_CAST") val row = each as? MutableMap ?: continue row.remove("password") row["amount_with_tax"] = Math.round(((row["amount"] as? Number)?.toDouble() ?: 0.0) * 1.18) } return null } ``` ## Do something after a write **Audit row and an event after a save** ```kotlin import am.G import am.Save fun main(g: G): Any? { if (!g.req.isApiRequestFromUser) return null val now = System.currentTimeMillis() // milliseconds since 1970 g.sys.db.saveSingleOrMultiple( Save( "mongodb", "shop", "audit_logs", mapOf( "api" to g.req.reqInfo.getObject("apiInfo").getString("id"), "by" to g.req.auth.getObject("authAMDB")["email"], "at" to now, "took" to now - g.res.shared.getLong("startedAt"), ), ), ) g.sys.system.emitEvent("order-placed", g.res.output) return null } ``` - For news to WebSocket clients, write nothing : the clients subscribed to the API get its answer after each successful call. Emit a WebSocket event yourself with `g.sys.system.emitEventWS` when the news 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.kotlin.md) page (automatic trigger). ## Streams Post hooks do not run on get all by stream and query by stream : the rows are on their way while they are read. To reshape a stream, read it in a custom API with `g.sys.db.getAllByStream`. ## Good to know - WebSocket news and the events of the API go out after the post hooks, with the final answer. - A level holds a list of post hooks, each one active or not. Each has a **Test** button on the API testing page, like pre hooks, and they deploy with Git. - [← Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) - [Events](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.kotlin.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.kotlin.md) : 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.kotlin.md#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.kotlin.md) 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 ```kotlin val text = g.sys.system.encrypt(mapOf("name" to "Joseph")) // with the transfer key of the secret val mine = g.sys.system.encryptWith("hello", "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.md), 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.md#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.kotlin.md#x-am-get-encrypted-data). An unknown `algorithm` answers `400`, and the message names it. ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md) 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.kotlin.md#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 ```kotlin val encryptedText = g.req.body.getString("encrypted") val value = g.sys.system.decrypt(encryptedText) // with the transfer key of the secret val mine = g.sys.system.decryptWith(encryptedText, "AES", "my-own-key") // with the algorithm and the key given ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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 hash of the body : an HMAC-SHA256, keyed with the `nonce` of the default secret (or its `secret` when there is no nonce). A text is hashed as it is, exactly like the `hashing` conversion of a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) hashes a column. So the hash of a tax id made here equals the one stored in the table, and finds the row. > **In short** > > - Send a value, get its hash as a hex text. > - The same value and the same secret always give the same hash. > - Use it to look up a column that the schema stores hashed. | | | |---|---| | 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.kotlin.md#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 **Find the customer whose tax id (hashed in the table) is known** ```kotlin val taxId = g.req.body.getString("taxId") val digest = g.sys.system.hash(taxId) val suppliers = g.sys.db.query(Query("mongodb", "shop", "suppliers").find(mapOf("tax_id_hash" to 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, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.md#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.kotlin.md#from-code) | - Video: [Call Your API Maker APIs from a Web or Mobile App: Tokens, CORS, Errors and Types](https://apimaker.dev/videos/call-apis-from-your-app) (8 min) ## 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.md#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.md) 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.kotlin.md) 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`. ```kotlin val apiUser = g.sys.system.getToken(GetToken("USER_NAME", "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. ```kotlin val databaseUser = g.sys.system.getToken( GetToken("USER_NAME", "PASSWORD") .name("TOKEN_GENERATOR_NAME"), ) // 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. ```kotlin val (apiUser, databaseUser) = g.sys.system.getTokens( listOf( GetToken("USER_NAME", "USER_PASSWORD"), GetToken("USER_NAME", "PASSWORD").name("TOKEN_GENERATOR_NAME"), ), ) // 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.md) 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. The token API is open to everybody : that is how a caller gets its first token. Its settings change that only when they name `authProviders`. An incomplete body answers `400` : `Please provide token request with ['u', 'p'] or ['refresh_token'].` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md#download-a-file) | | From code | [`g.sys.system.callExternalApi`](https://docs.apimaker.dev/v4/examples/sys/system/callExternalApi.kotlin.md) | ## 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.kotlin.md#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.kotlin.md#the-body). | | `auth` | Who asks : see [authentication](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-call-external-api.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md#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 ```kotlin val resp = g.sys.system.callExternalApi(CallExternalApi("https://api.example.com/customers/42")) if (resp.statusCode == 200L) g.logger.log(JsonObject.from(resp.body).getLong("id")) ``` A custom API which downloads an invoice from another server and sends it to its own caller : ```kotlin val saved = g.sys.system.callExternalApi( CallExternalApi("https://api.example.com/invoices/42/pdf") .auth(mapOf("type" to "bearer", "token" to "TOKEN")) .download(true) .throwOnError(true), ) // the file is in the uploads folder now : your code reads it at "uploads/" + saved.file.getString("path") return mapOf("__am__downloadFilePath" to saved.file.getString("path"), "__am__downloadFolderFileName" to saved.file.getString("name")) ``` - More examples in every language : [forms, files, XML, OAuth 2, retries and downloads](https://docs.apimaker.dev/v4/examples/sys/system/callExternalApi.kotlin.md). - 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. ## Errors of the call | Status | When | |---|---| | `400` | The body is wrong. The message names the key : `Invalid value for bodyType. Allowed values are [json, form, multipart, text, xml, binary].` Also a file that is not in the uploads folder, or a `download` that leads out of it. | | 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 itself becomes `500`, with the status in the `code` of the error. | | `500` | No answer came, or it can not be used : `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 : …`. | A pre hook can add the `auth` of the call. Then the secrets of the other server stay out of the reach of the caller. ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md) 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.kotlin.md) | ## 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.kotlin.md) 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.md) 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.kotlin.md), which works the same on every database. An unknown instance answers `404` : `Unable to get instance with name 'x'.` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.md) --- # 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.kotlin.md) 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.kotlin.md#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 ```kotlin val key = g.sys.system.getSecret("stripe.apiKey") val (key2, cs) = g.sys.system.getSecrets(listOf("stripe.apiKey", "common.connectionString.mysql_8")) val staging = 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, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md) 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.kotlin.md#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 ```kotlin val raw = g.sys.cache.getKey("session:user123") val session = raw?.let { Json.parse(it) } ``` ### Several keys at once - Get multiple redis keys values. ```kotlin g.sys.cache.getKeys(listOf("REDIS_KEY_NAME", "ANOTHER_REDIS_KEY_NAME")) ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md#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.md) (7200 by default) : keys never live forever. | | `NX` | `true` : write only when the key does not exist yet. | ## From code ```kotlin g.sys.cache.setKey("session:user123", Json.stringify(mapOf("cart" to 3)), 3600) g.sys.cache.setKeys(listOf(SetKey("captcha:abc", "7H2K").ttl(180).nx(true))) ``` - The [Redis dashboard](https://docs.apimaker.dev/v4/docs/logs/logs-and-dashboards.md#redis) shows the keys with their TTL and lets you edit them. ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md). 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.kotlin.md#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 ```kotlin g.sys.cache.removeKey("session:user123") g.sys.cache.removeKeys(listOf("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.kotlin.md), [custom](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.kotlin.md), [system](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-system-api.kotlin.md). ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md#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.kotlin.md#caching). ## From code ```kotlin g.sys.cache.resetCacheDB(ResetCacheDB("mysql8", "inventory", "customers")) ``` - One call for one answer : the header [`x-am-cache-control: reset_cache`](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#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.kotlin.md) instance only. Without a tenant, the cache of the table is reset for its structure database and every tenant. ```kotlin g.sys.cache.resetCacheDB(ResetCacheDB("crm::acme", "crm", "customers")) // the tenant acme g.sys.cache.resetCacheDB(ResetCacheDB("crm", "crm", "customers")) // the structure database and every tenant ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md#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 ```kotlin g.sys.cache.resetCacheCustomApis("customers-report") g.sys.cache.resetCacheCustomApisOfTenant("customers-report", "acme") ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.md) 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.kotlin.md#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 ```kotlin 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.kotlin.md) instance, with the [x-am-tenant-username](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md#x-am-tenant-username) header. Without a tenant, the cache of every tenant is reset too. ```kotlin g.sys.cache.resetCacheSystemApisOfTenant("SYSTEM_API_NAME", "acme") ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md#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 **A migration which adds a column once** ```kotlin val meta = g.sys.system.getTableMeta(GetTableMeta().instance("mysql8").database("inventory").table("order_transactions")) if (meta.none { it.name == "description" }) { g.sys.system.executeQuery(ExecuteQuery("mysql8", "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.kotlin.md) instance `crm`. In a custom API called for a tenant, `instance: "crm"` reads it in the database of that tenant. ```kotlin g.sys.system.getTableMeta(GetTableMeta("crm::acme", "crm", "customers")) ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.md) 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.kotlin.md#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. ```kotlin val results = g.sys.system.createIndexes( CreateIndexes() .indexes( IndexDefinition() .instance("mysql8") .database("inventory") .table("orders") .fields(IndexField("customer_id"), IndexField("status").order("DESC")), IndexDefinition("mongodb", "inventory", "products") .name("ux_products_sku") .unique(true) .fields(IndexField("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.md) 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.kotlin.md) instance `crm`. In a custom API called for a tenant, `instance: "crm"` creates it in the database of that tenant. ```kotlin g.sys.system.createIndexes( CreateIndexes() .indexes(IndexDefinition("crm::acme", "crm", "customers").fields(IndexField("email")).unique(true)), ) ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md#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. ```kotlin val results = g.sys.system.dropIndexes( 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, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md#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. ```kotlin val indexes = g.sys.system.getIndexes( 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, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md) 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.kotlin.md#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 ```kotlin val result = g.sys.system.emitEvent("order-placed", mapOf("order_no" to 1001), listOf("send-invoice")) g.logger.log(result.outputArr) ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md) 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.kotlin.md#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 ```kotlin g.sys.system.emitEventWS("default/orders-updated", mapOf("recipientId" to "42", "order_no" to 1001)) ``` An unknown event answers `400` : `WebSocket event not found with name …`. ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.md) 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.kotlin.md#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 ```kotlin val errors = g.sys.system.isValidDataForTable(IsValidDataForTable("mongodb", "shop", "products").data(g.req.body).sendingTo("SAVE")) if (errors.isNotEmpty()) { g.res.statusCode = 400 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. A table without a schema answers `400` : `Unable to find schema for …`. ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md) 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.kotlin.md#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 ```kotlin val draft = g.req.body val errors = g.sys.system.isValidDataForCustomAPI(IsValidDataForCustomAPI("Create Customer").type("BODY").data(draft)) ``` An unknown custom API answers `400` : `Unable to find custom API with name …`. ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md) 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.kotlin.md#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 ```kotlin g.sys.system.multiTenantInstanceUpdated(MultiTenantInstanceUpdated("crm", "acme")) ``` ### Several instances at once Several instances or tenants at once, each item answered on its own: ```kotlin val output = g.sys.system.multiTenantInstancesUpdated(listOf( MultiTenantInstanceUpdated("crm", "acme"), MultiTenantInstanceUpdated("billing", "acme"), )) // [{ success: true, data: true }, { success: false, data: false, error: { code: 404, message: "..." } }] ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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.kotlin.md#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.md) 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 ```kotlin val cs = g.req.body.getString("connectionString") val check = g.sys.system.isValidConnectionString(IsValidConnectionString(cs, "POSTGRE_SQL_DB")) if (!check.isSuccess) throw IllegalStateException("This database can not be reached : ${check.error}") ``` ## Access, headers and errors - Over HTTP, a system API answers only once its [settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) open it : `TOKEN_ACCESS` (the token of an API user whose [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.md) grants this system API) or `IS_PUBLIC`. Without settings it is `NO_ACCESS` : only your code (`g.sys`) and the admin panel reach it. - The settings can also keep the answer in the cache, or ask for the token of a person (`authProviders`). [Pre and post hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) run around it, as around any API. - It takes the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.kotlin.md) and answers the [envelope](https://docs.apimaker.dev/v4/docs/apis-all/response-format.kotlin.md). `401` : no valid token, or the API is `NO_ACCESS`. `403` : no group grants it. `400` : the body is wrong, and the message names the key. Related : [All APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#system-apis) · [System API settings](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md) · [System APIs from code](https://docs.apimaker.dev/v4/examples/sys/system/system.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 : your functions that run when the event is emitted. Each listener is written in its own [language](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md). Your code emits the event, a client emits it through a system API, or API Maker emits it after a successful call of an API you attach it to. So the side effects of an operation (mails, stock, exports) stay out of the API itself. > **In short** > > - An event is a name with listeners : your functions that run when the event happens. > - It happens when your code emits it, or by itself after an API you attach it to : an order saved. > - Use it for what comes after : an email, a stock update, an export. The API that saved the order stays simple. [![Events : the event orders/placed on the left, its listeners on the right](https://docs.apimaker.dev/assets/images/screens/ref-event.webp)](https://docs.apimaker.dev/assets/images/screens/ref-event.webp) 1. The events, in folders. 2. Its listeners : the code that runs when the event happens. > 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. - Video: [Event-Driven APIs: React to Saves, Code and HTTP Calls](https://apimaker.dev/videos/events) (5 min) ## 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 each successful call of the APIs listed under it. See [Trigger on API hit](https://docs.apimaker.dev/v4/docs/apis-all/events/user-created-events-api.kotlin.md#triggers). | | Trigger on APIs | APIs of tables, 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. | ## Trigger on API hit Turn on **Automatically trigger on API hit**, then add the APIs under **Trigger on APIs**. After each successful call of one of them, API Maker emits the event in the background : the caller does not wait for the listeners. - The event data (`g.req.eventData`) is the whole answer of that call : `{ success, statusCode, data, … }`. - A call that failed emits nothing. A call failed when its status is 400 or more, or when its answer says `"success": false`. - A call refused before the API ran (no token, no access) emits nothing either. - A call from your code (`g.sys.db…`) emits it too, like a call over HTTP. ## A listener ```kotlin import am.G import am.JsonObject import am.Save fun main(g: G): Any? { val data = JsonObject.from(g.req.eventData) // what was emitted g.sys.db.saveSingleOrMultiple( Save("mongodb", "shop", "notifications", mapOf("order_no" to data.getLong("order_no"), "text" to "Order ${data.getLong("order_no")} placed")), ) return mapOf("notified" to true) // returned to the emitter } ``` - 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. ## Emit from your code ```kotlin val result = g.sys.system.emitEvent("order-placed", mapOf("order_no" to 1001, "customer_id" to 42)) // result.outputArr : [ { listenerName: 'send-invoice', output: { notified: true } }, … ] g.sys.system.emitEvent("order-placed", mapOf("order_no" to 1001), listOf("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.kotlin.md). ## 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.kotlin.md) 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.kotlin.md), emitted with `emitEventWS` | | Something must happen on a schedule | A [scheduler](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md) | ## 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.md#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.md#log-profiles) records each listener run. - To emit over HTTP, the API user needs a group that grants the system API `EMIT_EVENT`. A group does not name single events. --- # 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 Launchpad, `wss://` behind its reverse proxy. A client connects with its tokens and subscribes to what it wants to hear about. Then it gets a message after each successful call of a subscribed API, on any server of the cluster, or when your code emits a WebSocket event. No polling, and no message broker of your own. > **In short** > > - Your app keeps a WebSocket open to API Maker, and hears about changes as they happen : no asking again and again. > - It says what it wants to hear about : a table, an API, or an event of your code. > - It sends its tokens, so it hears only about what it may read. > - It works across every server of a cluster. > 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. - Video: [WebSockets Without a Realtime Server: Push Live Data from Your APIs](https://apimaker.dev/videos/websockets) (5 min) ## 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.kotlin.md). ```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.md) 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 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.kotlin.md#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 each 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…). A call that failed sends nothing : its status is 400 or more, or its answer says `"success": false`. - For a custom WebSocket event, when your code emits it : ```kotlin g.sys.system.emitEventWS("default/orders-updated", mapOf("recipientId" to "42", "type" to "order-updated", "order_no" to 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 answer 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.md) 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.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md#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.kotlin.md), 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.kotlin.md#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.kotlin.md), 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.md) | **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.md) | **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.md) | **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.md) | **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.md) | **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.md) | **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.md) | **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.md). - **Count the servers from the memory** : the clients one server holds, plus one server as a spare. Every server of a cluster takes clients. They share the Redis of API Maker, which carries each notification to the server that holds the client : a load balancer needs no sticky session. - **Leave room** : keep a quarter of the memory free. Then the clients of a server that stops fit on the others. - **Clients come back together** after a restart. API Maker lets them in a few hundred at a time. A client that waits a random moment before it connects again helps (see [Staying connected](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.kotlin.md#staying-connected)). - **Too many WebSockets** : a part of the connections stays kept for the calls of your APIs. A client that comes when the server is full is answered `503` with `Retry-After: 5`. - **A server that is killed** cannot take its clients out of Redis. Another server does it within a minute. - **The limits of Linux** (open files, the connection table of the firewall, the memory of TCP) and how API Maker and Launchpad raise them : see [Scale to N users](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/scaling.md). ## 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.kotlin.md) 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. --- # 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, or a run on every server, once or in every runner, sharing the work with locks, 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 your function on a timer, written in [any language](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) of API Maker. It runs 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 server when one goes down. Or it [runs on every server](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md#run-on-every-server) at the same moment, and the runs share the work. > **In short** > > - A scheduler is your code on a timer : every minute, every night at two, or any cron. > - Each run happens once, on one server, even when you have many servers. > - Its time zone, its time limit and its next runs are on its page. [![A scheduler : its name and timeout, its intervals, and its next runs](https://docs.apimaker.dev/assets/images/screens/ref-scheduler.webp)](https://docs.apimaker.dev/assets/images/screens/ref-scheduler.webp) 1. Its name, how long it may run, and whether it is on. 2. When it runs : every minute, or a cron. 3. Its next runs, in its time zone. > 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. - Video: [Schedulers: Cron Jobs Without a Cron Server](https://apimaker.dev/videos/schedulers) (7 min) ## 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. | | Runs on | **One server** (each run happens once, on one server of the installation) or **Every server**, then **Once per server** or **Once per runner** : see [Run on every server](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md#run-on-every-server). | | 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 on the [native process](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md#native-process) of the server, not in a sandbox. | ## The code ```kotlin import am.G import am.RemoveByQuery fun main(g: G): Any? { val before = System.currentTimeMillis() - 7 * 86400000L // a week ago, in milliseconds since 1970 : what updated_at holds val removed = g.sys.db.removeByQuery(RemoveByQuery("mongodb", "shop", "carts").find(mapOf("updated_at" to mapOf("\$lt" to before)))) g.logger.log("${removed.deletedRowsCount} carts removed") return mapOf("removed" to removed.deletedRowsCount) } ``` - 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.kotlin.md#from-code) : the next run may be on another server. - Pick the language in the title bar of the editor. ## Intervals A scheduler has one or more intervals. Each interval has its time, its time zone and its own active switch. A scheduler with two intervals runs at the moments of both. ### The time of an interval | 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 | - The **time zone** of the interval says what "at 11:30" means : `Asia/Kolkata`, `Europe/Paris`, `UTC`… - Switch one interval off to pause it, and keep the others running. ### 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 A scheduler of **one server** (the default) : - **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. ## Run on every server - Video: [Schedulers on Every Server and Distributed Locks with Redis](https://apimaker.dev/videos/schedulers-every-server-and-locks) (7 min) **Runs on : Every server** runs the scheduler on every server of the installation at each moment of its intervals, for work every server does for itself (its own files, its own cache) or work the servers share. **Runs per server** says how many runs each server starts : | Runs per server | At each moment | |---|---| | Once per server | One run on each server. | | Once per runner | One run in each runner of the sandbox of the language of the code, on each server : as many runs as the server has processor cores (the runners a sandbox opens). The most work done at once. A runner which is not open yet opens, and runs the [process initializers](https://docs.apimaker.dev/v4/docs/features/process-initializers.kotlin.md) first. With **Run Native** : one run per server. | Every server starts the scheduler within a few seconds of a save, and a server which joins starts it too. The **Server** column of the Schedulers page and the Schedule panel of the form list the servers which run it ; each run on the panel says its server, and its runner. Each run is told where it is among the runs of its moment, in `g.req.scheduler` : | Key | Meaning | |---|---| | `worker`, `workers` | This run among all the runs of the moment, from 0, and how many there are, on all the servers. The servers number the runs the same way, server after server in the order of their ids. | | `firedAt` | The moment the runs were started for, in milliseconds since 1970 : the same in every run of that moment, on every server. | | `serverIndex`, `servers` | This server among the servers which run the scheduler, and how many they are. | | `runnerIndex`, `runners` | The runner of the run on its server, and how many runs this server started. | | `serverId`, `serverName`, `name`, `runOn` | The server, the scheduler, and where it runs. | Share the work between the runs of a moment : each run takes its share by `worker`, and the [lock](https://docs.apimaker.dev/v4/docs/features/locks.kotlin.md) of each item before it works on it, so an item a run of an earlier moment still works on is left alone. **Each run of the moment packs its share of the orders** ```kotlin import am.G import am.GetLock import am.Query import am.UpdateById fun main(g: G): Any? { val worker = g.req.scheduler.getLong("worker") val workers = g.req.scheduler.getLong("workers") val orders = g.sys.db.query(Query("mysql_8", "shop", "orders").find(mapOf("status" to "paid")).limit(500)) var packed = 0 for (order in orders) { val id = order.getLong("id") // Every run of the moment takes its share : the orders whose number, divided by the runs, leaves its own. if (id % workers != worker) continue // A run of an earlier moment may still work on an order : it holds its lock. val lock = g.sys.system.getLock(GetLock("order-$id").ttlSeconds(120.0)) if (!lock.isAcquired) continue try { g.sys.db.updateById(UpdateById("mysql_8", "shop", "orders", id).updateData(mapOf("status" to "packed"))) packed++ } finally { g.sys.system.releaseLock(lock.token) } } return mapOf("worker" to worker, "packed" to packed) } ``` - **Once per moment, on all the servers** : what must happen once (a summary, a mail), a run takes a lock named after the moment, `report-${g.req.scheduler.firedAt}`, with `ttlSeconds` longer than the work and no release : one run of all the servers gets it. - **Overlapping moments** : a moment does not wait for the runs of the one before. A run which may outlast the interval takes locks for its items, as above. - **Without locks** : `worker` and `workers` alone give each run its own items, as long as every run ends before the next moment. ## 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.md#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.md#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. Or run it [on every server](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md#run-on-every-server), in every runner, and share the rows between the runs. - No group opens a scheduler to an API user : a scheduler runs on its timer, or with **Run now** in the admin panel. --- # 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 is a function of one argument : `g`. That holds for a custom API, a hook, an event listener, a scheduler, a migration, a process initializer and a test case. `g` holds the request, the answer, and every API of API Maker. ```kotlin import am.G import am.GetAll fun main(g: G): Any? { val rows = g.sys.db.getAll(GetAll("mongodb", "shop", "products")) g.logger.log(rows.size, "products") return rows } ``` | | | |---|---| | `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.md#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.kotlin.md). 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.md) · [Python](https://docs.apimaker.dev/v4/docs/languages/python.md) · [PHP](https://docs.apimaker.dev/v4/docs/languages/php.md) · [C#](https://docs.apimaker.dev/v4/docs/languages/csharp.md) · [Go](https://docs.apimaker.dev/v4/docs/languages/go.md) · [Rust](https://docs.apimaker.dev/v4/docs/languages/rust.md) · [Java](https://docs.apimaker.dev/v4/docs/languages/java.md) · [Kotlin](https://docs.apimaker.dev/v4/docs/languages/kotlin.md). 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.kotlin.md#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 parts of the URL : `apiPath`, `instanceName`, `dbEnvironment` (the database of the URL), `collection`, `id`, `primaryKey`, `field` and `order` of distinct, `tenantUsername` for a [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.kotlin.md) 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.kotlin.md) 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. | | `scheduler` | In a scheduler : this run among the runs of the same moment, `worker` of `workers`, `firedAt`, its server and runner. A scheduler which [runs on every server](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md#run-on-every-server) shares its work by them. | **Who is calling** ```kotlin val person = g.req.auth.getObject("authAMDB") // the row of your users table, minus its password val apiUser = g.req.auth.getObject("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. | **An HTML page from a custom API** ```kotlin g.res.contentType = "text/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.kotlin.md#files). ## g.sys | Part | Holds | |---|---| | `g.sys.db` | The [schema APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) 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.kotlin.md). | | `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.kotlin.md#without-a-schema). | | `g.sys.system` | The [system APIs](https://docs.apimaker.dev/v4/examples/sys/system/system.md) : `encrypt`, `decrypt`, `hash`, `getToken`, `getSecret`, `callExternalApi`, `executeQuery`, `getTableMeta`, `createIndexes`, `dropIndexes`, `getIndexes`, `emitEvent`, `emitEventWS`, `isValidDataForTable`, `isValidDataForCustomAPI`, `multiTenantInstanceUpdated`, `isValidConnectionString`. And from code only, the [locks](https://docs.apimaker.dev/v4/docs/features/locks.kotlin.md) every run on every server shares : `getLock`, `releaseLock`. | | `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.kotlin.md#from-code). | | `g.sys.test` | In a [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md) only : `runCustomApi`, `mock`, `clearMocks`, `calls`. | - Every `g.sys.db` call takes the same `headers` and `queryParams` an HTTP call takes. - Your code is trusted : the groups do not decide which APIs it may call. Its calls carry the tokens of the request, so the column permissions of the caller's groups still apply. Check who calls, with `g.req.auth`, before you read or write for them. - 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 ```kotlin val order = mapOf("_id" to "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… val count = g.res.shared.getLong("count") // …read in the post hook of the same request g.shared.merge("visits", 1) { old, _ -> (old as Int) + 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.kotlin.md) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) · [Code examples](https://docs.apimaker.dev/v4/examples/index.kotlin.md) · [Cheat sheet](https://docs.apimaker.dev/v4/docs/cheat-sheet.kotlin.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 that all your code shares : helpers, checks, price rules, clients of other services, interfaces. Write it once on **APIs & Logic → Utility Classes**, in TypeScript or [another language](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.kotlin.md#other-languages). Then use it in all the code of that language. > **In short** > > - In TypeScript, import it with `import * as Tax from 'utils/Tax'`. Each other language has its own way, below. > - A class is used by the code of its own language only. > - Save it, and the next request uses it : nothing restarts. - Video: [Utility Classes and Process Initializers: Shared Code, Loaded Once](https://apimaker.dev/videos/utility-classes-and-initializers) (6 min) ## 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. **The utility class Calculator** ```kotlin object Calculator { fun add(x: Double, y: Double) = x + y fun multi(x: Double, y: Double) = x * y } ``` **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.kotlin.md#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) : **The utility class Inf** ```kotlin data class UserP(val firstName: String, val lastName: String, val phone: Long) ``` **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)` | **The utility class Tax** ```kotlin object Tax { const val RATE = 0.2 fun add(amount: Double): Double = Math.round(amount * (1 + RATE) * 100) / 100.0 } ``` **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.kotlin.md#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.kotlin.md#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. --- # 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 is your code that runs when a runner of your sandbox starts, before that runner serves any request. A runner is one process of a sandbox. Use it for what every process needs : a subscription to a message broker, a client kept for the life of the process, a warm cache. > **In short** > > - It runs once in every runner : a sandbox has up to one runner per processor core. > - The active initializers run one after the other, in their order. > - Keep it light and safe to run twice. | | | |---|---| | Page | `APIs & Logic → Initializers` : name, order, timeout in minutes, active, and the code. | | Runs | In every runner of a sandbox, when it starts. The active initializers run one after the other, in their order. | | Code | The function `main` of any [language](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md), with the same `g` as a custom API. | | Test | **Test API** runs it on the API testing page. | **Keep the rates in the memory of the process** ```kotlin import am.G import am.GetAll fun main(g: G): Any? { // Read once per process : every request this process serves finds them in g.shared. val rates = g.sys.db.getAll(GetAll("mysql_8", "shop", "rates")) g.shared["rates"] = rates g.logger.log(rates.size, "rates loaded") return null } ``` - 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.md#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. - How many sandboxes run is set on the [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) page. Each runner of each of them 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.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) · [Schedulers](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md) --- # Locks > g.sys.system.getLock and g.sys.system.releaseLock in API Maker - one run at a time (or a few) does what a lock guards, on every server and in every runner, in all eight languages. Kept in Redis by the agent of each runner, freed by itself when its time is over, waited for or given up at once. Source: https://docs.apimaker.dev/v4/docs/features/locks.html `g.sys.system.getLock` takes a lock, and `g.sys.system.releaseLock` gives it back. A lock is a name in your account. While one run of your code holds it, the other runs that ask for the same name do not get it. That holds on every server, in every sandbox and runner, in every language. [Schedulers that run on every server](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md#run-on-every-server) share their work this way : each run locks an item before it works on it. > **In short** > > - `getLock('orders')` answers at once : `acquired` is `true` or `false`. > - A lock frees itself after `ttlSeconds` (60 by default), so a run that crashes never blocks the others. > - Give it back in a `finally`. `maxHolders` lets a few runs hold it together. - Video: [Schedulers on Every Server and Distributed Locks with Redis](https://apimaker.dev/videos/schedulers-every-server-and-locks) (7 min) | | | |---|---| | Take | `getLock('name')`, or `getLock({ name, ttlSeconds, waitSeconds, tries, retryDelayMs, maxHolders, token, throwIfNotAcquired })` | | Give back | `releaseLock(lock)`, or `releaseLock(lock.token)` | | Frees itself | after `ttlSeconds`, 60 by default : a run which crashes never blocks the others | | Waits | not at all by default ; with `waitSeconds` it gets the lock the moment it is given back | | A few at a time | `maxHolders: 3` : three runs hold it at once, the fourth waits | | Where | the Redis of your installation, reached by the agent of each runner itself : the call never goes through the server | | Code only | in every kind of code of your account ; no system API answers it over HTTP | ## Take a lock, give it back The name says what the lock guards : the invoice of an order, a report, a sync with a partner. A run which does not get the lock answers at once (`acquired` is `false`) : it leaves the work to the run which holds it. Give the lock back in a `finally`, so a run which fails gives it back too. **Send the invoice of an order once** ```kotlin import am.G import am.GetLock import am.UpdateById fun main(g: G): Any? { val id = g.req.body.getLong("id") // One run at a time works on an order : a run which asks meanwhile finds the lock taken, and leaves it. val lock = g.sys.system.getLock(GetLock("invoice-$id").ttlSeconds(30.0)) if (!lock.isAcquired) return mapOf("sent" to false) try { g.sys.db.updateById(UpdateById("mysql_8", "shop", "orders", id).updateData(mapOf("invoice_sent" to true))) return mapOf("sent" to true) } finally { g.sys.system.releaseLock(lock.token) } } ``` ## Wait for a turn, a few at a time `waitSeconds` waits while others hold the lock : the call gets it as soon as a holder gives it back (Redis tells every server at once, the call does not wait for its next try), or when a hold runs out. `maxHolders` lets several runs hold the lock together : a service which takes three calls at a time gets three, never a fourth. `throwIfNotAcquired` turns a lock which was not taken into an error, so the code after the call only runs with the lock. **Three calls of the payment service at a time** ```kotlin import am.CallExternalApi import am.G import am.GetLock fun main(g: G): Any? { // The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails. val lock = g.sys.system.getLock(GetLock("payment-service").maxHolders(3).waitSeconds(10.0).ttlSeconds(20.0).throwIfNotAcquired(true)) try { val resp = g.sys.system.callExternalApi( CallExternalApi("https://payments.example.com/charges").method("POST").body(mapOf("order" to g.req.body.getLong("id"))), ) return resp.body } finally { g.sys.system.releaseLock(lock.token) } } ``` ## Keep a lock longer A lock frees itself `ttlSeconds` after it was taken : give a time longer than the work, or keep the lock longer while the work goes on. `getLock` with the `token` of a lock your code holds keeps that hold for `ttlSeconds` more, with the same token, and takes nothing else. **Long work : keep the lock at every page** ```typescript const lock = await g.sys.system.getLock({ name: 'nightly-export', ttlSeconds: 60 }); for (let page = 0; lock.acquired && page < pages; page++) { await exportPage(page); // Sixty seconds more for the next page. await g.sys.system.getLock({ name: 'nightly-export', token: lock.token, ttlSeconds: 60 }); } ``` `releaseLock` answers `false` when the hold had ended before : the time was over, and another run may have held the lock meanwhile. That says the work took longer than its `ttlSeconds`. ## What getLock takes | Key | Default | Meaning | |---|---|---| | `name` | required | The name of the lock : any text of up to 256 characters, of your account only. Give the name alone for the defaults : `getLock('orders')`. | | `ttlSeconds` | 60 | The lock frees itself this long after it was taken, or kept longer : even when the code which holds it never gives it back. Fractions are allowed, up to 30 days. | | `waitSeconds` | 0 | How long to wait while others hold it. 0 answers at once. | | `tries` | 1 | How many times to ask at most, `retryDelayMs` apart. With `waitSeconds` : as many as the wait leaves room for. | | `retryDelayMs` | 100 | The pause between two tries. A release wakes a waiting call at once, whatever the pause. | | `maxHolders` | 1 | How many runs hold the lock at once. Give the same number in every place which asks for the name. | | `token` | | The token of a lock you hold : keeps it for `ttlSeconds` more. | | `throwIfNotAcquired` | `false` | A lock which was not taken is an error (status 400), not an answer. | ## What getLock answers | Key | Meaning | |---|---| | `acquired` | Whether your code holds the lock now. | | `name` | The name of the lock. | | `token` | What says that your code holds it : `releaseLock` and a `getLock` which keeps the lock longer take it. Empty when the lock was not taken. | | `expiresAt` | When the hold ends, in milliseconds since 1970. When the lock was not taken : when the first hold of the others ends. | | `ttlSeconds` | How long the lock is held. | | `holders`, `maxHolders` | How many hold it now, your code among them when it took it, and how many may. | | `tries`, `waitedMs` | How many times it was asked for, and how long the call waited. | `releaseLock` takes the lock or its token, and answers `true` when your code held the lock until then. A lock which was not taken gives nothing back (`false`). Both take a last `true` (or `g.sys.system.full`) for the whole answer, as every call does. ## In a scheduler of every server A scheduler which runs on every server, or in every runner of every server, starts several runs at the same moment. [`g.req.scheduler`](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.kotlin.md#run-on-every-server) says which one a run is : `worker` among `workers`, the same moment `firedAt` on every server. Two ways to share the work, often together : - **Each run its share** : the items whose number divided by `workers` leaves `worker`. No call is needed to know whose item it is. - **A lock per item** : a run takes the lock of an item before it works on it ; an item whose lock is taken is someone else's. A run of an earlier moment which still works on an item keeps it. - **Once per moment** : a lock named after the moment (`report-${g.req.scheduler.firedAt}`, `ttlSeconds` longer than the work) is taken by one run of all the servers : that run alone does what must happen once. ## How it works - A lock is a small sorted set in the Redis of the installation : the tokens of its holders, each with the moment its hold ends, by the clock of Redis itself. No two servers disagree on the time, and a hold of a server which went away ends by itself. - Taking a lock, keeping it longer and giving it back are one script of Redis each : one trip, a fraction of a millisecond on the network of the servers. - The agent of each runner, in the sandbox or in the process of the server's machine, talks to Redis on its own connection : a call of `getLock` never goes through the server. A call which waits is woken the moment the lock is given back, on any server : Redis publishes the release to the servers which have a call waiting. - A server whose settings name no Redis is alone : it keeps its locks in its process. ## Good to know - The names are of your account : another account has locks of the same names of its own. - Choose `ttlSeconds` longer than the work it guards ; for long work, keep the lock longer at each step. - A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md) mocks `g.sys.system.getLock` as any other call. - The [debugger](https://docs.apimaker.dev/v4/docs/features/debugging.md) shows the answer of `getLock` among the variables : a run which waits on a breakpoint holds its lock, until its `ttlSeconds` are over. --- # 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 A test case checks a custom API or a utility class of your account, with mocks in place of the database and the web. - Test cases run on the server, in the sandbox of your account, with the same `g` your custom APIs get. Run them from **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.kotlin.md#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. [![Test cases : the tests in the editor, the run, and a failed test with its reason](https://docs.apimaker.dev/assets/images/screens/ref-test-cases.webp)](https://docs.apimaker.dev/assets/images/screens/ref-test-cases.webp) 1. Your tests. 2. **Run**. 3. A test that failed, and why. - Video: [Test Cases with Mocks and Code Coverage for Your Custom APIs](https://apimaker.dev/videos/test-cases) (7 min) ## 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.kotlin.md#mocks) to the calls of `g.sys` which must not reach the database or the web. 3. **Run them** **Run** on the page (`Ctrl+E`, or `Cmd+E` on a Mac, in the editor), 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.kotlin.md#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. - Video: [Code Coverage of Backend Tests in Eight Languages](https://apimaker.dev/videos/code-coverage) (7 min) - **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+E` (`Cmd+E` on a Mac) 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 : ```kotlin import am.G // A test case of Kotlin : every function whose name starts with test and which takes g is a test. What it throws is // its failure. // // val output = g.sys.test.runCustomApi(RunCustomApi("orders").body(mapOf("amount" to 100))) // g.sys.test.mock(Mock("db.getAll").whenever(mapOf("collection" to "customers")).returns(listOf(mapOf("id" to 1)))) fun testCaseName1(g: G) { val sum = 4 + 6 check(sum == 10) { "4 + 6 is $sum, not 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.kotlin.md#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 : - Video: [Mocks That Fail, Count and Compute : Test Cases in 8 Languages and in CI](https://apimaker.dev/videos/test-cases-in-depth) (8 min) ```kotlin import am.AmErrors import am.Count import am.G import am.GetById import am.JsonObject import am.Mock import am.Query fun testMocksAnswerValuesErrorsAndFunctions(g: G) { g.sys.test.mocks( listOf( // a value, when the query has these keys (inner objects too) Mock("db.query").whenever(mapOf("collection" to "orders", "find" to mapOf("status" to "paid"))).returns(listOf(mapOf("id" to 1, "total" to 20), mapOf("id" to 2, "total" to 30))), // an error, for the first call only : the next call gets the next mock Mock("db.count").error(mapOf("message" to "The database is busy.", "code" to 503)).times(1), Mock("db.count").returns(2), // a function of the arguments of the call Mock("db.getById").answers { args -> val id = (args[0] as JsonObject).getLong("id") mapOf("id" to id, "total" to id * 10) }, ), ) val rows = g.sys.db.query(Query("mysql_8", "shop", "orders").find(mapOf("status" to "paid", "customer" to 7))) check(rows.size == 2) { "2 rows were expected" } try { g.sys.db.count(Count("mysql_8", "shop", "orders")) error("the first count did not fail") } catch (busy: AmErrors) { check(busy.code == 503) { "refused with ${busy.code}" } } check(g.sys.db.count(Count("mysql_8", "shop", "orders")) == 2L) { "2 was expected" } val order = g.sys.db.getById(GetById("mysql_8", "shop", "orders", 4)) check(order.getLong("total") == 40L) { "a total of 40 was expected" } check(g.sys.test.calls("db.count").size == 2) { "two calls were expected" } // 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. ```kotlin import am.AmErrors import am.G import am.JsonObject import am.Mock import am.RunCustomApi fun testOrderCountAnswersTheNumberOfOrders(g: G) { g.sys.test.mock(Mock("db.getAll").whenever(mapOf("collection" to "orders")).returns(listOf(mapOf("id" to 1), mapOf("id" to 2), mapOf("id" to 3)))) val output = JsonObject.from(g.sys.test.runCustomApi(RunCustomApi("orderCount").query(mapOf("customer" to 7)).headers(mapOf("x-role" to "admin")))) check(output.getLong("count") == 3L) { "3 orders were expected" } // the whole answer : data, logs, headers, warnings val full = g.sys.test.runCustomApiFull(RunCustomApi("orderCount").query(mapOf("customer" to 7))) check(full.logs == listOf("3 orders")) { "the log of the count was expected" } // what the API throws is thrown here try { g.sys.test.runCustomApi(RunCustomApi("orderCount").body(mapOf("fail" to true))) } catch (refused: AmErrors) { check(refused.message?.contains("failed on purpose") == true) { "refused with ${refused.message}" } return } error("orderCount did not fail") } ``` - `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} ``` **Its test case** ```kotlin import am.AmErrors 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"] } } fun testTooHeavyIsRefused(g: G) { try { g.sys.test.runCustomApi(RunCustomApi("shippingCost").body(mapOf("weight" to 40))) } catch (refused: AmErrors) { check(refused.code == 422) { "refused with ${refused.code}" } return } error("a parcel of 40 kg was accepted") } ``` - 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 : ```kotlin import am.G fun testTheTaxOf100(g: G) { check(Tax.add(100.0) == 120.0) { "the tax of 100 is not 20" } } ``` - `Tax` is the utility class of [the page of the utility classes](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.kotlin.md#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.kotlin.md#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.kotlin.md#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. --- # Developer Tools > Swagger docs per API user, TypeScript interfaces from your schemas, client code in 21 languages, a debugger for the code of every language, an IDE in the admin panel 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 : docs for the people who call them, types for your code, a page to try every API, a debugger, and your own editor in sync with the server. > **In short** > > - **Swagger docs** for each API user : only the APIs it may call. > - **API Testing** tries every API, with its hooks, and shows the logs and the database calls. > - **The local client** keeps a folder of your computer in sync, for VS Code, WebStorm or an AI agent. - Video: [Developer Tools: Swagger, Redis Dashboard, Typed Rows and Shortcuts](https://apimaker.dev/videos/developer-tools) (5 min) ## 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 an OpenAPI 3.0 document. Paste it in the **Swagger Docs** page of the dashboard to read the APIs and try them, or give it to any tool which reads OpenAPI, like a generator of clients. - An API which asks for several tokens says so in the document : a call has to carry all of them. An API which takes files gets a form with its fields of files. - 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.kotlin.md#editor). ## API testing page - Video: [API Testing in the Panel: Call, Debug, Export and Share Any API](https://apimaker.dev/videos/api-testing) (6 min) - 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.md#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.md#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.kotlin.md#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 - Stop the code of every language at breakpoints, in the sandboxes and on the native process. The admin panel is the debugger : nothing to install. - Turn it on in **Debug Settings**, set a breakpoint in the margin of the editor, and send the request from API Testing. See [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## IDE - Every file of your code in one editor : search in all the files, go to file, tabs, go to definition in every language, the debugger, and the history of each file in git with its lines annotated. Open it from the dashboard, or with the **IDE** button of any code editor. - Learn more [IDE](https://docs.apimaker.dev/v4/docs/features/ide.md). ## Your own editor - Video: [Code Your Backend in VS Code: Two-Way Sync with the Local Client](https://apimaker.dev/videos/vs-code-local-client) (5 min) - `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.kotlin.md). - A file you save is sent at once, a tenth of a second after your editor wrote it, and what the panel or another developer saves is written to your disk as soon as the server saved it. The files the local client writes are not sent back ; a change you type right after them is sent like any other. - While it is connected, API Maker also scans the account on its own, and writes a changed report to `src/api_security_report/` on your disk. So an AI assistant working in the folder reads a fresh report. API Maker commits nothing : you commit when you want. Learn more [APIs Security Report & Actions](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.md). - 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.md#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', name: 'Alice', // optional : how the others see this local client (else its user and computer) }; }; ``` ### Several developers, one sync token - Every developer of the account runs the local client with the same sync token, each in a clone of their own : up to 50 at a time. What one of them saves reaches the others, and the admin panel, within a second. - The others get what the server saved, written again from the database, never the files as they were sent. A file a developer takes out of a folder (a page of an SSR website) is taken out on the other computers too, unless it was changed there. - Each line of the terminal says where a change comes from, and what happened : ```text 👥 Joined : [10:32:05] : Bob (bob@ThinkPad, shop-api) · 2 local clients sync this account ✓ Send : [10:32:41] : [change] : src/custom_apis/Cart_Quote/Cart_Quote.ts ✓ Saved on server : [10:32:41] : src/custom_apis/Cart_Quote ✓ Receive : [10:33:02] : src/schemas/products ← the admin panel (Mayur) ✓ Receive : [10:33:15] : src/ssr/shop ← the local client of Bob (bob@ThinkPad, shop-api) ✓ Remove : [10:33:15] : src/ssr/shop/pages/old.tsx ← the local client of Bob (bob@ThinkPad, shop-api) ✅ Full Sync : [10:34:00] ← a git pull ``` - The bar of the admin panel shows who is connected : the faces of the local clients and their count. Its menu lists each one with its computer, its folder, its address and since when, and links to the sync token : a new token disconnects every local client. ### A channel nobody can read or take over - The local client connects inside the encrypted channel of the admin panel : the keys of each connection are agreed on (ECDH P-256), and the address of the socket and every message are sealed (AES-256-GCM), each in its order. - The sync token never travels. The local client proves that it knows it with a proof bound to its connection, and the server proves it in its turn : nothing is sent to a server which does not. Who stands between the two reads nothing, and can not use what it saw elsewhere. The token never shows in the logs of a proxy either. - Whatever the server sends, the local client writes inside its folder only : `src/` and `tsconfig.json`. - An address which gives 20 wrong sync tokens is refused for 15 minutes. A `localClient.js` of before still connects, with the token in the clear : the panel marks it with an open lock until it is updated from git. - Over the internet, keep HTTPS / WSS in front of API Maker all the same. The handshake goes to the same address as `webSocketURL` (`wss://` → `https://`) : point it at API Maker, whose APIs and web sockets share one port. --- # IDE > Every file of your code in one editor in the admin panel - an explorer of the repository, search in all the files, go to file, tabs, go to definition in every language, back and forward, saves, the debugger, and the history of each file in git with its lines annotated. Source: https://docs.apimaker.dev/v4/docs/features/ide.html Every file of the code of your account in one editor : custom APIs, utility classes, schedulers, events, hooks, migrations, test cases, auth providers, settings, schemas and translations. The files are laid out as your [Git repository](https://docs.apimaker.dev/v4/docs/Git/git.md) has them. > **In short** > > - Open it from **APIs & Logic → IDE**, or with the **IDE** button of any code editor. > - Search all files, go to a definition in every language, keep many tabs. > - With Git connected : who wrote each line, and the history of each file. - Video: [An IDE in the Browser for Your Backend Code: Go to Definition, Search, Git Blame](https://apimaker.dev/videos/ide) (7 min) - **From the dashboard** : `APIs & Logic → IDE` opens it on the whole repository. **Dashboard** in its bar takes you back. - **From any code editor** : the **IDE** button at the top right of the editor opens it around the code of the page, which stays its first tab and is saved by the page. **Close IDE** gives you the editor of the page again ; the IDE keeps its tabs for the next time. | | | |---|---| | Explorer | The folders of `src`, one per kind of item, and the files of the projects of the languages at the root. The YAML files API Maker writes for each item are not shown. | | Search | Every file at once, with match case, whole word and regular expressions. Right click a folder or a file : **Search in this folder**. | | Go to file | The letters of a name, in order. | | Tabs | Drag them, close them with the middle button of the mouse, right click for **Close others**, **Close to the right**, **Close saved**. | | Go to definition | In every language : a name of the same file, a utility class, the declarations of API Maker. **Back** and **Forward** go through the places you went. | | Saves | A file is saved into its item, with the checks of its page. A file saved elsewhere since you opened it asks what to keep. | | Debugger | With debugging on, the panel below the editor has a **Debug** tab and the IDE opens the file where the code stopped. See [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). | ## The panel below the editor Opened from the [API testing page](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#api-testing-page), the IDE shows the tabs of its right side below the editor : **Response**, **Logs**, **Docs**, **API Docs** (a custom API) and **Index Maker**, with **Debug** while debugging is on. **Send** in the bar of the IDE sends the request. Drag the line above the panel to resize it. ## Git : who wrote each line, and the history of a file Once the [Git page](https://docs.apimaker.dev/v4/docs/Git/git.md) connects a repository to the account : - **Annotate with Git Blame** (`Alt + A`, the right click menu of the editor, of a tab or of a file of the explorer) shows, left of each block of lines, the day and the author of the commit which wrote it. The colour of the bar is the age of the commit. Hover it for the commit and its message ; click it for the history of the file at that commit. Lines changed since the last commit say **Not committed yet**. `Alt + A` again closes the annotations. - **Git History of This File** (the same menus) opens every commit which changed the item of the file, compared with the version before or any other. A version can be put back in its tab, as changes you save or not. A file API Maker writes from the items (`package.json`, `tsconfig.json`, the declarations) has no history of its own. ## Keys None of the keys of the IDE is a key of the browser's tabs : `Cmd/Ctrl + W` and `Cmd + Option + Left / Right` of a Mac stay the browser's. | Action | Mac | Windows / Linux | |---|---|---| | Go to file | `Cmd + P` | `Ctrl + P` | | Search in the files | `Cmd + Shift + F` | `Ctrl + Shift + F` | | Explorer | `Cmd + Shift + E` | `Ctrl + Shift + E` | | Show or hide the side | `Cmd + B` or `Cmd + \` | `Ctrl + B` or `Ctrl + \` | | Show or hide the panel | `Cmd + J` | `Ctrl + J` | | Back, forward | `Ctrl + Option + Left`, `Ctrl + Option + Right` | `Alt + Left`, `Alt + Right` | | Close the tab | `Option + W` | `Alt + W` | | Annotate with Git Blame | `Option + A` | `Alt + A` | | Save, save all | `Cmd + S`, `Cmd + Option + S` | `Ctrl + S`, `Ctrl + Alt + S` | The back and forward buttons of the mouse go back and forward too. --- # Debugging > Stop the code of your custom APIs, hooks, schedulers and utility classes at breakpoints, in every language, from the admin panel, in sandboxes and on the native process. Source: https://docs.apimaker.dev/v4/docs/features/debugging.html Stop your code where it goes wrong, and look inside : the values of its variables, the calls that led there, the value of any expression. Step through it line by line, into a utility class and back. All of it happens in the admin panel, in the **Debug** tab of the [API testing page](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#api-testing-page). > **In short** > > - Turn on **Debugging** in **Debug Settings**, then set breakpoints in the margin of the editor. > - Send the request from API Testing : the code stops there, in every language. > - Other clients ask for the debugger with the header `x-am-run-in-debugger: true`. > - Use it on development servers : a stopped runner holds its request. [![The debugger : the line where the code stopped, and its variables](https://docs.apimaker.dev/assets/images/screens/ref-debug.webp)](https://docs.apimaker.dev/assets/images/screens/ref-debug.webp) 1. The line where the code stopped. 2. Step over, continue. 3. The variables at this moment. - Video: [Debug an API from the Failing Call to the Line That Broke](https://apimaker.dev/videos/debugging) (9 min) - Video: [Debug Your Backend in Every Language: Breakpoints in the Browser](https://apimaker.dev/videos/debugging-every-language) (6 min) - **Every language** : TypeScript and JavaScript (Node.js, Bun), Python, PHP, C#, Go, Rust, Java and Kotlin. - **Every piece of code** : custom APIs and their hooks, the hooks of instances, databases, tables and system APIs, listeners of events, schedulers, process initializers, migration scripts, WebSocket events, token generators, utility classes and test cases. - **Wherever it runs** : in the sandboxes of your account, and on the [native process](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md#native-process) of the server (`runOnNativeProcess`). - **Nothing to install** : no IDE, no tunnel, no port to open. The admin panel is the debugger. ## Turn it on - Open **Debug Settings** in the menu of your user, turn **Debugging** on and **Save**. It is on for every server of the account ; nothing restarts. - From then on, a request which asks for the debugger runs in a **debug runner** : the code stops at your breakpoints there. The other requests run as always. - The requests of the API testing page ask for the debugger by themselves while debugging is on. Any other client asks with the header `x-am-run-in-debugger: true` : the code stops while the API testing page is open in the admin panel. - A debug runner is the **debug sandbox** of the environment of the code (one per language and group of separate sandboxes), or, for code which runs on the native process, the **debug runner of the server** for your account. It starts with the first request which asks for it. A request which waits at a breakpoint is not timed out for an hour. - Turn debugging off and save : every debug runner of the account ends, on every server. ## The Debug tab With debugging on, the API testing page opens on its **Debug** tab, the first tab of the right side, before **Response**. The page debugs for as long as it is open : the code stops at the breakpoints, and the tab shows where. The tab is there only while debugging is on, and only for code : a custom API, an event, a scheduler, a hook and the other items which hold code. An API of a table or a system API has no Debug tab, unless you try one of its hooks. When code stops at a breakpoint, the tab shows whatever is open, so you can let the code go on. - **The toolbar** : what the code does now (running, or paused and where), and the buttons below. - **Variables** : the scopes of the frame (locals, closures, globals), each variable opens to its members. - **Watch** : expressions evaluated again at every stop, kept by your browser. - **Call Stack** : the frames which led there. Click one to see its line and its variables. The frames of the runner, of the packages and of the runtime are hidden : **Show** brings them. - **Breakpoints** : every breakpoint of the account by file : switch one off, go to its line, take them all out. - **Console**, below them : what the code prints as it runs (`console.log`, `print`, `echo`, `fmt.Println`, `println!`, `Console.WriteLine`, `System.out.println`), what the logpoints wrote, and any expression evaluated where the code stopped (the arrows bring back the earlier ones). Drag the line above it to give it more room. What the code prints is in the console of the account too. When the code stops, the page shows the Debug tab, and the tab header says it with an amber dot. A response which comes while another tab is shown puts a green dot on **Response**. Drag the line between the request and the response to give each side its width (double-click it to put it back in the middle) : the tabs and the editors of each side follow. ### Breakpoints - Click left of a line number in the code editor (or **F9** on the line of the cursor) : a red dot is a breakpoint. Click it again to take it out. - Right click the margin for more : - **Add Conditional Breakpoint…** : the code stops only when the expression is true (`total > 100`, `user.role == "admin"`, `len(items) > 3`). - **Hit Count** : it stops from the nth time on (`3`, `>= 3`), only the nth time (`== 3`), past the nth time (`> 3`), or every nth time (`% 2`). - **Add Logpoint…** : instead of stopping, it writes a message to the console of the debugger, each `{expression}` in it evaluated where the code is (`total is {total}`). Its mark is a diamond. - **Disable Breakpoint** keeps it, grey, and the code does not stop there. **Remove Breakpoint** takes it out. - A hollow dot is a breakpoint which is not bound yet : the code of the file has not run in the debug runner since it was set (it binds when it does). - The breakpoints are the account's : every tab and every server of the account has them, and they follow the lines when you edit the code above them. Set them in the code editor of the request, or in the utility classes the header of the page opens. ### Where the code stopped Send the request : the code stops at the breakpoint, its line is shown in the editor, with the values of the variables at the ends of the lines above it. | Action | Key | | |---|---|---| | Continue | **F5** | to the next breakpoint | | Step Over | **F10** | to the next line | | Step Into | **F11** | into the function this line calls : a utility class too | | Step Out | **Shift+F11** | back to the code which called this function | | Run to Line | **Ctrl/Cmd+F10**, or right click the margin | to the line of the cursor | | Pause | | stops the code where it runs | | Pause on exceptions | | stops where an exception is thrown, caught or not ; a panic of Go and Rust | - **Hover** a name in the editor while the code is stopped : its value. - A file where the code stops which no editor of the page shows, a [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.kotlin.md) the code stepped into for example, opens at the top of the Debug tab, with its breakpoints : the steps go on there, and back. - When several runners stopped (two languages, two servers), the tab shows one : choose another in the list of its toolbar. - Leave the page, or turn debugging off : the runners let the code go, it no longer stops at the breakpoints. > **Java and Kotlin** > > The debugger evaluates the variables, their fields, the items of arrays, literals and operators (`total > 100 && items.length == 3`). It calls no method. ## Code on the native process Code with `runOnNativeProcess: true` runs in a runner of the server itself : a request which asks for the debugger runs in the **debug runner of the server** for your account, never in the runner every account shares. The server makes the debug engine of the language ready the first time : debugpy for Python, Delve for Go, netcoredbg for C#, in a folder of its own. Node.js, Bun and the JVM have theirs. Rust needs `lldb-dap` on the server (Xcode on macOS, LLVM on Linux) and PHP the extension Xdebug : the answer of the request says how to install what is missing. On a Mac, Go and Rust are debugged through the debugserver of Xcode, which macOS lets take hold of the code only once the developer mode of the Mac is on. Turn it on once with `sudo DevToolsSecurity -enable` ; until then the request says so, and the same code can be debugged in its sandbox. ## Security Nothing but the server reaches the debugger of your code, and what it shows travels encrypted. - **Between the admin panel and the server**, every request of the debugger and every event of it (where the code stopped, what the code printed) goes inside the [encrypted channel of the admin panel](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.md) : the server answers the people of the panel there alone, never in the clear. Only the people of the account reach its debugger. (The channel keeps what it carries from who only listens ; HTTPS also proves to the browser that your server answers.) - **In a sandbox**, the debug engine listens on the loopback of the container : no port of the machine publishes it, and no other container or process reaches it over the network. The server comes through the agent of the sandbox, which takes only the connections that carry the token of that sandbox. - **On the native process**, each engine is reached where only the server is let in : the inspector of Node.js names its target by an id the runner tells the server alone, the inspector of Bun answers at a secret path of its runner, debugpy takes no request before the secret of its runner, Xdebug connects to a socket of the server and gives the secret its runner holds, Delve listens on a socket only the user of the server reaches, netcoredbg and lldb-dap are started by the server for each connection, and the JVM connects to the server itself as it starts. The secrets are new for each debug runner. - The debug runners of an account are the account's alone : the runners every account shares never debug. - The sandboxes themselves are no wall against hostile code : [what they protect, and what they do not](https://docs.apimaker.dev/v4/docs/security/overview.md#sandboxes). ## Good to know - Debugging is for your development servers : a runner stopped at a breakpoint holds its request. - A request which asks for the debugger while debugging is off runs as every other request. - The time a request waits at a breakpoint is not counted against its time : it waits for an hour. - In Go and Rust, what the code printed just before it stopped can show in the console only once the code goes on : their runner reads it on a thread the debugger stops too. --- # 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. > **In short** > > - **In a sandbox** (best) : give the API a group of sandboxes whose Dockerfile installs Chromium. > - **On the native process** : install Chromium on each server yourself. No isolation. > - Plan 2 GB of RAM for a browser, and avoid many at once. | | 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.md#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.kotlin.md#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.md#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.kotlin.md#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.kotlin.md) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) · [How API Maker works](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.md#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.kotlin.md#one-language) : its code and all its hooks are written in the same language and run on the same runtime. > **In short** > > - Write your code in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin. Each piece of code picks its own. > - Pick the language above the code editor : it offers a template. > - Every language gets the same `g` : the request, the answer, and every API of API Maker. > - An API and its hooks share one language. | Language | Runtimes | Image (default version) | Packages from | `main` | |---|---|---|---|---| | [TypeScript / JavaScript](https://docs.apimaker.dev/v4/docs/languages/typescript.md) | Node.js, Bun | `node:24-slim`, `oven/bun:1-slim` | npm | `async function main(g)` | | [Python](https://docs.apimaker.dev/v4/docs/languages/python.md) | Python | `python:3.13-slim` | PyPI | `def main(g)` or `async def main(g)` | | [PHP](https://docs.apimaker.dev/v4/docs/languages/php.md) | PHP 8.3+ | `php:8.4-cli` | Packagist (Composer) | `function main($g)` | | [C#](https://docs.apimaker.dev/v4/docs/languages/csharp.md) | .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.md) | Go | `golang:1.27` | Go modules | `func Main(g *am.G) (any, error)` | | [Rust](https://docs.apimaker.dev/v4/docs/languages/rust.md) | Rust | `rust:1-slim` | crates.io | `pub fn main(g: &mut G) -> Result` | | [Java](https://docs.apimaker.dev/v4/docs/languages/java.md) | 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.md) | 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.kotlin.md) : 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). - Video: [One API in 8 Languages: TypeScript, Python, PHP, C#, Go, Rust, Java, Kotlin](https://apimaker.dev/videos/eight-languages) (5 min) ## The same API in every language The API reads `name` from the body, counts the rows of a table and answers both : ```kotlin import am.Count import am.G fun main(g: G): Any? { val count = g.sys.db.count(Count("mysql_8", "inventory", "customers")) return mapOf("hello" to g.req.body.getString("name", "world"), "customers" to count) } ``` ## 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.md#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 tokens 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.kotlin.md#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.md#debugger), for a run of [test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.kotlin.md) (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.md#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 Launchpad 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.md#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.kotlin.md#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.md#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.kotlin.md#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.md)) and of a token generator, the settings of the APIs of a [table](https://docs.apimaker.dev/v4/docs/settings/apiSettings.md), of a [database](https://docs.apimaker.dev/v4/docs/settings/databaseSettings.md) and of a [system API](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.md), a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.md), the texts of [internationalization](https://docs.apimaker.dev/v4/docs/i18/i18.kotlin.md) and a [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.kotlin.md). - 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.kotlin.md#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.kotlin.md) or a [hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.kotlin.md) 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.md) (`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.md#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.md), 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.kotlin.md) 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.md) 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.kotlin.md#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.kotlin.md#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.kotlin.md#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.md#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.md#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.kotlin.md#native-process) runs with the version of its language you install for it in [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md#native-languages) (on every server, from the publisher of the language), or 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.md#languages-for-native-apis) installs one on the machine too, 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.md#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.kotlin.md#octet-stream) one in every language. - Video: [Install Node.js, Python, Go or Java on Every Server, per Admin](https://apimaker.dev/videos/native-languages-panel) (6 min) - Video: [Node.js, Python, Go and Java for Native APIs, Installed for You](https://apimaker.dev/videos/native-languages) (8 min) - Video: [Sandboxes, Runtimes and Packages: Where Your Code Runs](https://apimaker.dev/videos/sandboxes-and-runtimes) (7 min) ## Related - [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) · [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.kotlin.md) · [Separate sandboxes of a custom API](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.md#separate-sandbox) --- # 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.md) : **Node.js** (the default) or **Bun**, in the version you pick there, with only the npm packages you add. > **In short** > > - Write `async function main(g)` and export it : `module.exports = main`. > - It runs on Node.js (24) or Bun (1), as [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) say. > - Refuse a request with `throw [{ code: 400, message }]`. **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.kotlin.md), utility classes, `types` and the [examples of `g.sys`](https://docs.apimaker.dev/v4/examples/index.kotlin.md) 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.kotlin.md), [generated APIs](https://docs.apimaker.dev/v4/examples/sys/db/getAll.kotlin.md#without-a-schema), [system APIs](https://docs.apimaker.dev/v4/examples/sys/system/system.md), [cache](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-set-redis-key-api.kotlin.md#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.md#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.kotlin.md) 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.kotlin.md#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. ## Debugging - Turn debugging on in the debug settings, set a breakpoint in the margin of the editor and send the request : the code stops on the line of your TypeScript, on Node.js and on Bun alike (its source map is applied for you), and the **Debug** tab of the API testing page shows its variables and its console. - Learn more [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) · [Code examples](https://docs.apimaker.dev/v4/examples/index.kotlin.md) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) --- # 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.md) name another version), with only the packages you add from PyPI. > **In short** > > - Write `main(g)`, as a function or a coroutine. > - It runs on `python:3.13-slim`, unless the settings name another version. > - Refuse a request with `raise g.Errors(message, code=400)`. **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.kotlin.md) 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.kotlin.md#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.md#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.md#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.kotlin.md#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)`, `get_lock`, `release_lock` | | `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) | ## Debugging - The debug runner of Python runs debugpy, which the server alone reaches : the **Debug** tab of the API testing page shows where the code stopped. - A conditional breakpoint takes an expression of Python (`len(items) > 3`), a logpoint `{expression}`s of Python. The steps go past the runner and the packages (just my code). - Learn more [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) · [g.sys.db examples](https://docs.apimaker.dev/v4/examples/sys/db/getAll.kotlin.md) --- # 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.md) name another version, 8.3 at least), with only the packages you add from Packagist. > **In short** > > - Write `function main($g)`. > - It runs on `php:8.4-cli`, unless the settings name another version. > - Refuse a request with `throw new AmErrors($message, 400)`. **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.kotlin.md) 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.kotlin.md#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.md#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.kotlin.md#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)`, `getLock`, `releaseLock` | | `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`). ## Debugging - The debug runner of PHP loads Xdebug, which connects to the server for each request : the **Debug** tab of the API testing page takes it while the page is open, else the code runs on. - Write variables as PHP does in a condition, a watch or a logpoint (`$total > 100`, `{$cart->name}`). - Learn more [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) --- # 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.md) name another version), with only the packages you add from NuGet. > **In short** > > - Write a class with `public static async Task Main(G g)`. > - It is compiled once, and runs on `mcr.microsoft.com/dotnet/sdk:10.0` unless the settings name another version. > - Refuse a request with `throw new AmErrors(message, 400)`. **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.md#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.kotlin.md) 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.kotlin.md#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.md#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.kotlin.md#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)`, `GetLock`, `ReleaseLock` | | `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` | ## Debugging - The debug runner of C# compiles your code with its symbols, under its file of the repository : netcoredbg attaches to it for the **Debug** tab of the API testing page. - Learn more [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) --- # 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.md) name another version), with only the modules you add. > **In short** > > - Write `func Main(g *am.G) (any, error)` in a package that imports `apimaker/am`. > - It is built once, on `golang:1.27` unless the settings name another version. > - Refuse a request with `return nil, am.Fail(400, message)`. **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.md#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`, a key of the cache a `*string` (`nil` when it is not there), 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.kotlin.md) 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.kotlin.md#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.md#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.md#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.kotlin.md#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)`, `GetLock`, `ReleaseLock` | | `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{…}}` | ## Debugging - The debug runner of Go builds your code without optimizations and runs under Delve : the **Debug** tab of the API testing page steps through it. - A panic stops the code when **Pause on exceptions** is on. - Code on the native process of a Mac needs the developer mode of the Mac (`sudo DevToolsSecurity -enable`) : Delve works through the debugserver of Xcode. - Learn more [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) --- # 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.md) name another version), with only the crates you add. > **In short** > > - Write `pub fn main(g: &mut G) -> Result`. > - It is built once, on `rust:1-slim` unless the settings name another version. > - Refuse a request with `Err(am::fail(400, message))`. **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.kotlin.md) 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.kotlin.md#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.md#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.kotlin.md#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)`, `get_lock`, `release_lock` | | `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()` | ## Debugging - The debug runner of Rust builds your code without optimizations, with its symbols : lldb-dap attaches to it for the **Debug** tab of the API testing page. - A panic stops the code when **Pause on exceptions** is on. - Code on the native process of a Mac needs the developer mode of the Mac (`sudo DevToolsSecurity -enable`) : lldb-dap works through the debugserver of Xcode. - Learn more [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## Related - [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) --- # 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.md) name another version), with only the packages you add from Maven Central. > **In short** > > - Write a class with `static Object main(G g)`. > - It is compiled once, and runs on `eclipse-temurin:25-jdk` unless the settings name another version. > - Refuse a request with `throw new AmErrors(message, 400)`. **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.md#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.kotlin.md) 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.kotlin.md#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.md#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.md) 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.kotlin.md#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)`, `getLock`, `releaseLock` | | `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` | ## Debugging - The debug runner of Java speaks JDWP with the server : the **Debug** tab of the API testing page shows where the code stopped. - Every piece of code of Java is a class `Api` : the debugger tells the classes of each file apart. - The debugger evaluates variables, fields, items of arrays and operators : it calls no method. - Learn more [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## Related - [Kotlin](https://docs.apimaker.dev/v4/docs/languages/kotlin.md) · [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) --- # 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.md) name another version), with only the packages you add from Maven Central. > **In short** > > - Write `fun main(g: G): Any?` at the top of the file. > - It is compiled once, and runs on `eclipse-temurin:25-jdk` unless the settings name another version. > - Refuse a request with `throw AmErrors(message, 400)`. **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.md#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.md#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.kotlin.md) 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.kotlin.md#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.md#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.md) 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.md#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.kotlin.md#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)`, `getLock`, `releaseLock` | | `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` | ## Debugging - The debug runner of Kotlin speaks JDWP with the server : the **Debug** tab of the API testing page shows where the code stopped. - The debugger evaluates variables, properties, items of lists and operators : it calls no function. - Learn more [Debugging](https://docs.apimaker.dev/v4/docs/features/debugging.md). ## Related - [Java](https://docs.apimaker.dev/v4/docs/languages/java.md) · [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.kotlin.md) --- # 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.kotlin.md#basic-info) your code is written in, and it is what the [schema APIs](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) enforce. The [generated APIs](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-get-all-api.md#without-a-schema) never read it. > **In short** > > - A schema says what a row of a table looks like : each field, its type, its rules, its default value. > - The schema APIs use it on every write : they clean the values, check them, and refuse a bad row with `400`. > - It also knows the links between tables, so `deep` can read related rows. > - Write it in the panel, or press **Generate** to get a first one from the table. [![The schema of the table products in its editor](https://docs.apimaker.dev/assets/images/screens/ref-schema.webp)](https://docs.apimaker.dev/assets/images/screens/ref-schema.webp) 1. One field : its type, its rules, its default value. > 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.kotlin.md) check. | | Gives | Validation, conversions, defaults, relations, the [TypeScript interface](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#typescript-interfaces-from-your-schemas) `db...I
`, the sample payloads of the API testing page and of Swagger. | - Video: [Table Schemas in TypeScript, Checked on Every Write](https://apimaker.dev/videos/table-schemas) (7 min) ## 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.md) 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.md) 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.kotlin.md). 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.kotlin.md) 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.kotlin.md)). | `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.md). ### 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.kotlin.md#g-sys-in-the-sandbox). The schema itself is [settings](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md#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.md#packages) | | Code in another [language](https://docs.apimaker.dev/v4/docs/languages/index.kotlin.md) : 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.md). 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.md). | **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 - Video: [Virtual Fields: One-to-Many Relations Across Databases](https://apimaker.dev/videos/virtual-fields) (5 min) **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.md) (`deep` on any get), [master save](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-master-save-api.md) (one call saves the order and its items) and the [ER diagram](https://docs.apimaker.dev/v4/docs/logs/logs-and-dashboards.md#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. - Video: [Create Databases and Tables in the Panel: One Designer, Six Engines](https://apimaker.dev/videos/table-designer) (13 min) - Video: [Vectors, JSON, Bytes, Shapes and Intervals Through One REST API, on Six Databases](https://apimaker.dev/videos/data-types) (6 min) - Video: [Encrypted and Hashed Fields, Auto Increment and Optimistic Locking](https://apimaker.dev/videos/encryption-ids-and-locking) (7 min) ## Related - [Optimistic concurrency control](https://docs.apimaker.dev/v4/docs/features/optimistic-concurrency-control.md) · [Auto increment](https://docs.apimaker.dev/v4/docs/features/auto-increment.md) · [Deep populate](https://docs.apimaker.dev/v4/docs/apis-all/query-params/deep.md) · [Is valid data for table](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-table-api.kotlin.md) - [Generated interfaces](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#typescript-interfaces-from-your-schemas) · [Error codes](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) --- # 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.md) in step. > **In short** > > - Make and change tables with the mouse, on every kind of database : no SQL to write. > - A new table gets its schema and its APIs at once. > - A change made outside API Maker is found, and the schema follows it. [![The Table Designer : the name of the table, its columns, and common columns in one click](https://docs.apimaker.dev/assets/images/screens/ref-table-designer.webp)](https://docs.apimaker.dev/assets/images/screens/ref-table-designer.webp) 1. The name of the table, and a comment. 2. Common columns in one click : ids, dates. 3. The columns : name, type, not null, unique, default value. 4. **Create table**. ## 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.kotlin.md) 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.md). **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.md#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.md), their indexes and their [settings](https://docs.apimaker.dev/v4/docs/settings/collectionSettings.md). **+** opens the [table designer](https://docs.apimaker.dev/v4/docs/schema/table-designer.md#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.md#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.md#schema-sync). | | APIs | The [operations of the table](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md#the-17-operations-of-a-table), each with its [settings](https://docs.apimaker.dev/v4/docs/settings/apiSettings.md) and **Test API**, which opens the [API testing page](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#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.kotlin.md) 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.md) 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.kotlin.md#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.md) · [Database migration](https://docs.apimaker.dev/v4/docs/features/database-migration.kotlin.md) · [All APIs at a glance](https://docs.apimaker.dev/v4/docs/apis-all/overview.kotlin.md) --- # 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. 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.md#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.md) and `g.sys.db` from every language : save, get by id, update, query. > **In short** > > - A vector is a list of numbers. A JSON column holds any JSON value. > - Bytes are a `Buffer` object : `{ "type": "Buffer", "data": [1, 2, 3] }`. > - A shape goes in as GeoJSON, and comes back in the form of the driver of the database. | | | |---|---| | 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. | - Video: [Vectors, JSON, Bytes, Shapes and Intervals Through One REST API, on Six Databases](https://apimaker.dev/videos/data-types) (6 min) ## 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.md) 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.md#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.kotlin.md#save-vectors-json-and-bytes) for the code of all eight languages. ## Related - [Table schema](https://docs.apimaker.dev/v4/docs/schema/schema.md) · [Connection strings](https://docs.apimaker.dev/v4/docs/Database-connection-string/mysql-connection-strings.md) · [Save single or multiple](https://docs.apimaker.dev/v4/examples/sys/db/saveSingleOrMultiple.kotlin.md) --- # 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. > **In short** > > - `order_no: { __type: EType.number, isAutoIncrementByAM: { start: 1000, step: 5 } }` > - One counter per field, in the Redis of API Maker, shared by every server : no two rows get the same number. > - See and change the counters on **Database → Auto Increments**. | | | |---|---| | 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.md#5-keys-and-generated-values) · [Git integration](https://docs.apimaker.dev/v4/docs/Git/git.md) --- # 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 erases the first one, and nobody notices. With a version field, the second save is refused : the app can reload the row and ask. > **In short** > > - Add one field with `isConcurrencyControlField: true`, which gets a new value on every save. > - Send back the version you read with every update. An old or missing version answers `400`. > - Update by id and replace by id check it. Update many does not. | | | |---|---| | 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.md) and [replace by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-replace-by-id-api.md) 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`. | - Video: [Encrypted and Hashed Fields, Auto Increment and Optimistic Locking](https://apimaker.dev/videos/encryption-ids-and-locking) (7 min) ## 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.md) · [Update by id](https://docs.apimaker.dev/v4/docs/apis-all/schema-apis/auto-generated-schema-based-update-by-id-api.md) · [Error codes](https://docs.apimaker.dev/v4/docs/apis-all/error-codes.md) --- # 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 set of APIs for many customers, each with a database of its own. > **In short** > > - Keep your tenants in a table : one row per customer, with the connection string of its database. > - Mark the instance as multi-tenant, and point it at that table. > - Name the tenant in each request : `crm::acme` in the path, or `x-am-tenant-username: acme`. | | | |---|---| | 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. - Video: [Multi-Tenant APIs: One Set of APIs, a Database per Tenant](https://apimaker.dev/videos/multi-tenant) (5 min) ## 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.kotlin.md#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 ``` **The tenant of the request, and its data** ```kotlin import am.G import am.GetAll import am.Query fun main(g: G): Any? { val tenant = g.req.params.getString("tenantUsername") // "acme", null without the header // The database of acme : no need to name the tenant. val customers = g.sys.db.getAll(GetAll("crm", "crm", "customers")) val orders = g.sys.db.gen.queryGen(Query("crm", "crm", "orders").find(mapOf("status" to "paid"))) return mapOf("tenant" to tenant, "customers" to customers, "orders" to orders) } ``` - Another tenant, or the structure database, on purpose: **Name the tenant of one call** ```kotlin // In the instance name : the tenant of the name wins over the tenant of the request. val customersOfGlobex = g.sys.db.getAll(GetAll("crm::globex", "crm", "customers")) // With the header of the call. val ordersOfGlobex = g.sys.db.count( Count("crm", "crm", "orders") .headers(mapOf("x-am-tenant-username" to "globex")), ) // An empty header : the structure database. val templates = g.sys.db.getAll( GetAll("crm", "crm", "templates") .headers(mapOf("x-am-tenant-username" to "")), ) ``` - Queries and table meta run in the database of the tenant too: **SQL of the tenant of the request** ```kotlin val rows = g.sys.system.executeQuery( ExecuteQuery("crm", "select status, count(*) as orders from orders group by status") .database("crm"), ) val columns = g.sys.system.getTableMeta(GetTableMeta().instance("crm").database("crm").table("orders")) // Another tenant : name it in the instance. val rowsOfGlobex = g.sys.system.executeQuery(ExecuteQuery("crm::globex", "select count(*) from orders").database("crm")) ``` - A custom API with `enableCaching` keeps its answers per tenant. Reset them for one tenant: **Reset the cache of a custom API for one tenant** ```kotlin g.sys.cache.resetCacheCustomApisOfTenant("customers-report", "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.kotlin.md#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. **Post hook : an audit row in the database of the tenant** ```kotlin import am.G import am.Save fun main(g: G): Any? { g.sys.db.saveSingleOrMultiple(Save("crm", "crm", "audit_logs", mapOf("action" to "customer saved", "tenant" to g.req.params["tenantUsername"]))) return null } ``` ## 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: **Reset the cache of a table** ```kotlin g.sys.cache.resetCacheDB(ResetCacheDB("crm::acme", "crm", "customers")) // acme only g.sys.cache.resetCacheDB(ResetCacheDB("crm", "crm", "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.kotlin.md) 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. ```kotlin g.sys.system.multiTenantInstanceUpdated(MultiTenantInstanceUpdated("crm", "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. --- # 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, fill a value, create an index. It is code of the account, kept in Git. API Maker runs each script **once per environment**, in order. > **In short** > > - After a Git pull or a deployment hook, the active scripts not yet run on this environment run, by `order`. > - Write scripts that can run twice without harm. > - The **REC** pill of the panel can write the script for you, as you change tables. [![A new migration script : its name, label, order and timeout](https://docs.apimaker.dev/assets/images/screens/ref-migration.webp)](https://docs.apimaker.dev/assets/images/screens/ref-migration.webp) 1. A name. 2. What it does, in words. 3. Scripts run in this order. 4. How long it may run. | | | |---|---| | 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.md#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.kotlin.md), with the same `g` as a custom API. Test it on the [API testing page](https://docs.apimaker.dev/v4/docs/features/developer-tools.md#api-testing-page). | - Video: [Database Migrations in TypeScript or Python, Once per Environment](https://apimaker.dev/videos/database-migrations) (7 min) ## A script **Add a column once** ```kotlin import am.ExecuteQuery import am.G import am.GetTableMeta fun main(g: G): Any? { val meta = g.sys.system.getTableMeta(GetTableMeta().instance("mysql8").database("inventory").table("order_transactions")) val hasDescription = meta.any { it.name == "description" } if (!hasDescription) { g.sys.system.executeQuery( ExecuteQuery("mysql8", "ALTER TABLE `inventory`.`order_transactions` ADD COLUMN `description` varchar(255) NULL AFTER `qty`"), ) } return mapOf("hasDescription" to hasDescription) } ``` - [Get table meta](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-table-meta-api.kotlin.md) tells what is there, [execute plain query](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-execute-plain-query-api.md) runs any SQL or MongoDB command, [create indexes](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.kotlin.md) 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.md) (**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.kotlin.md). | | 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.md#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.md) 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.md) : 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.md) · [Execute plain query](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-execute-plain-query-api.md) · [Create indexes](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-create-indexes-api.kotlin.md) · [Table designer](https://docs.apimaker.dev/v4/docs/schema/table-designer.md) --- # Import & Export of Databases > Export a table, a database or a whole instance of MongoDB, PostgreSQL, MySQL, MariaDB, SQL Server or Oracle from the API Maker admin panel with the database's own programs (mongodump, pg_dump, mysqldump, mariadb-dump, SqlPackage, Data Pump), download the file, and import it, or a file those programs wrote, into any instance of the same kind : compared with the database of now first, and chosen table by table. The programs are installed from their publishers when needed, the version most compatible with each database unless an admin chose one on Sandbox Settings. Source: https://docs.apimaker.dev/v4/docs/features/import-export.html Copy a table, a database or a whole instance into one file. Then load that file into any instance of the same kind : another server, another database name, or a table under a new name. The database's **own programs** make and read the files (mongodump, pg_dump, mysqldump, SqlPackage, Data Pump). So a file holds what the database itself would write, and you can restore it by hand, without API Maker. > **In short** > > - Export from the **⋮** menu of a card of **APIs & Logic → Instance API (DB API)** : an instance, a database, or the tables you select. > - Import a file into an instance of the same kind of database. > - The tools are downloaded from their publishers the first time they are needed. [![Import into Database : the file compared with the database, table by table](https://docs.apimaker.dev/assets/images/screens/ref-import.webp)](https://docs.apimaker.dev/assets/images/screens/ref-import.webp) 1. Each table of the file, compared with the database. 2. What will change. - Video: [Import and Export Databases: mongodump, pg_dump, mysqldump and More](https://apimaker.dev/videos/import-export) (10 min) | | | |---|---| | Where | The **⋮** menu of the cards : **Instances** (a whole instance), **Databases** (a database), **Collections / Tables** (the tables you selected). Each opens the Import & Export dialog : **Export**, **Import** and **Jobs**. | | Databases | MongoDB, PostgreSQL, MySQL, MariaDB, SQL Server, Oracle. | | Programs | mongodump, mongorestore, mongoexport, mongoimport · pg_dump, pg_restore, pg_dumpall, psql · mysqldump, mysql · mariadb-dump, mariadb · SqlPackage · Oracle Data Pump, run by the database itself. | | Files | A `.zip` with the files of the program, `manifest.json` (what it holds) and `README.txt` (how to restore it by hand). An import also takes the program's own files : an archive of mongodump, a dump of pg_dump, a script of SQL, a `.bacpac`, a file of Data Pump, JSON or CSV. | | Compare | Before an import, each table of the file beside the table of the database of now : new, changed (rows, columns, indexes), the same, there now only. You choose what each one becomes. | | Jobs | Run in the background on a server, two at a time on each server, **gently** if you ask ; the dialog follows them, with their log. The files stay 24 hours by default, then only the job and its log. | | Tools | Downloaded from their publishers the first time a job needs them, into `__import_export_tools` on each server : the version most compatible with each database, or the one an admin chose on [Sandbox Settings](https://docs.apimaker.dev/v4/docs/features/import-export.md#the-tools-on-sandbox-settings). | | Backups | The same exports, on schedules into S3 : [Automatic Backup](https://docs.apimaker.dev/v4/docs/features/automatic-backups.md). | ## Export 1. Open the **⋮** menu of the card and choose **Export Instance**, **Export Database** or **Export Tables** (**Collections** for MongoDB). 2. **What to export** : the whole instance, one database (a schema for Oracle), or some tables of it. 3. **Form of the file** : the forms of the database, below. 4. **Options** : what the program of the database offers, each with its help behind the **?**. 5. **Export**. The job runs on the server : its progress, its log, then **Download** and **Send to a storage** (a storage of [Automatic Backup](https://docs.apimaker.dev/v4/docs/features/automatic-backups.md), where the backups of that folder are listed and restored). The file stays on the server for the hours of [Sandbox Settings](https://docs.apimaker.dev/v4/docs/features/import-export.md#the-tools-on-sandbox-settings) (24 by default) — the dialog says until when — then it is removed : database dumps are large. The job and its log stay. The top of the dialog names the program the jobs of the instance run on this server, with its version (`18.6 · installed`, `100.13.0 · of the server`), or *installed from* its publisher *on first use*. It is checked again when a job ends : once the export which installed the program is done, the line says its version — as the tab [Import & Export Tools](https://docs.apimaker.dev/v4/docs/features/import-export.md#the-tools-on-sandbox-settings) of Sandbox Settings does. **Gently**, beside the button, runs the programs of the job at the least priority of the server — `nice 19` and the idle class of I/O on Linux, the background mode of macOS, the idle class of Windows : the APIs of the server go first, the job takes longer. It is there for imports too. The download is a link the server makes for this file, good for 15 minutes : the browser downloads the file itself, a file of gigabytes included. | Database | Forms of the file | |---|---| | MongoDB | **Archive of mongodump** (documents, indexes, options, views) ; for collections also **JSON** or **CSV**, one file per collection (mongoexport) | | PostgreSQL | **Dump of pg_dump** (custom format, restored in parallel) or **Script of SQL** ; a whole instance adds the roles (`pg_dumpall --globals-only`) | | MySQL, MariaDB | **Script of mysqldump** / **mariadb-dump**, compressed, with views, routines, triggers and, for a whole instance, the users and their grants | | SQL Server | **.bacpac of SqlPackage** (schema and data), or a **Script of T-SQL** written by the server for a database or some tables : the tables, their rows and their foreign keys, which goes into any database | | Oracle | **File of Data Pump**, written by the database itself (`DBMS_DATAPUMP`) : a schema, some tables, or the whole database | > **Tip : No program on the server for SQL Server on ARM** > > Microsoft publishes SqlPackage for x86-64 only. On an ARM server, export a database of SQL Server as a **Script of T-SQL**. ### Options of an export | Database | Options | |---|---| | MongoDB | a filter of JSON for one collection, the fields of JSON or CSV, one array of JSON or a document per line, canonical extended JSON, the users and roles of the database, a point in time (oplog) for a whole replica set, views as collections, where to read from (a secondary keeps the load off the primary), collections at once | | PostgreSQL | schema only, data only, without owners, without grants, without comments, INSERT statements (with column names) instead of COPY, `DROP … IF EXISTS` first, compression, only some schemas, without some tables, without the rows of some tables (logs, caches), the roles with or without their passwords | | MySQL, MariaDB | schema only, data only, routines, triggers, events, `DROP TABLE` first, `INSERT IGNORE` or `REPLACE`, many rows or one row per INSERT, column names in each INSERT, a condition for the rows of one table, the users and their grants | | SQL Server | check the schema before the data (SqlPackage), the time a statement may take ; for the script : schema only, data only, replace the tables, rows per INSERT | | Oracle | objects and rows, rows only or objects only, a condition for the rows of one table, every table at one moment (`FLASHBACK_SCN`), statistics, the whole database (`FULL`), a dump an older Oracle reads, workers, the directory object | ## Import 1. Open the **⋮** menu and choose **Import into Instance**, **Import into Database** or **Import Tables**. 2. **The file** : drop or choose a file of your computer. It is sent at once, no button to press : in pieces of 4 MB, a piece which fails on the way is sent again, up to 64 GB. If the sending fails, **Send again**. **Another file** replaces it ; an import whose file was sent but which was never started is removed from the server when you take another file or close the dialog. 3. **What it holds** : the kind of the file, the instance and the version of the database it comes from, the program which made it, its size and SHA-256, its databases and their tables. 4. **Where it goes** : every database of the file under its own name, one database of the file into the database you name (made when it is not there), or some tables of the file. One table can take another name (MongoDB, MySQL, MariaDB, Oracle) : it is a copy, which goes in whole. 5. **Compared with the database of now** : **Table by table** (below), or **The whole file, with its options**. 6. **Import**. In a whole import, an option which removes or replaces what is there — replace the database, replace the collections, remove the objects first, empty or replace the tables, delete the documents of the file — asks you to type the name of the database before the import starts. ### Table by table Once the file is read, each of its tables is set beside the table of the same name in the database it goes into : the rows, the bytes, the columns and the indexes of both. An export of API Maker describes its tables as they were (its `manifest.json`), so the comparison is exact ; the database of now is read from its catalog, its rows counted up to 200,000 rows a table (estimated beyond). > Diagram : An import table by table : the file beside the database of now > > An import table by table : each table of the file is set beside the table of the same name in the database of now. A table the database lacks is new, one whose rows, columns or indexes differ is changed, one which is equal is the same, and a table only the database has is there now only. For each one you choose : replace it by the file's, merge the rows of the file which are not there, keep it as it is, or remove a table the file does not have. What replaces or removes rows asks for the name of the database first, and every table is counted again after the import. | The table is | Means | You choose | |---|---|---| | **New** | the database of now does not have it | **Replace** (it is made as the file has it) or **Keep** (left out) | | **Changed** | its rows, columns or indexes differ : the difference of the rows is shown, each column and index added, removed or changed | **Replace**, **Merge** or **Keep** | | **The same** | the same rows, columns and indexes | **Keep**, or **Replace** to have it again | | **There now only** | the file does not have it | **Keep**, or **Remove** it | - **Replace** : the table becomes the one of the file — its rows, columns and indexes. What it holds now goes. - **Merge** : the rows of the file which are not there are added ; the rows there now stay as they are. MongoDB adds the documents whose `_id` is not there ; PostgreSQL, MySQL, MariaDB, SQL Server and Oracle the rows whose key is not there (a table of PostgreSQL without a key takes the rows it does not have). - **Keep** : the table is left as it is. - **Remove** : a table only the database has is dropped, with its rows. **Restore the backup** (the database as the file has it), **Only the changes** (the new and changed tables, nothing else touched) and **Nothing** choose for every table at once ; a search and **Only what differs** narrow the list (a table you chose stays in it). When a choice replaces or removes rows, the dialog names those tables with their rows and asks for the name of the database. After the import, each table chosen is counted again : its rows now beside the rows of the file, **ok** when they agree. A file of some tables only — an export of a few tables or collections, a file of JSON, CSV or a `.bson` of one collection — says nothing of the other tables of the database : they are not in the comparison, none of them is **There now only**, and none can be removed by the import. The dialog says so above the list. Such an export dropped back into the dialog opens the import on those tables. A file whose tables can not be read before it is imported goes in whole, with the options below ; a file without the description of an export of API Maker is compared by the names of its tables. A script of SQL of PostgreSQL is compared, to be seen, and goes in whole : pg_restore takes some tables of a dump of its own format only. A file of JSON, CSV or a `.bson` of MongoDB goes in whole too, into the collection of its name (`people.json` : `people`) or the one you name. So does a table imported under another name : it is a copy, which changes nothing there now. A file of JSON or CSV of no document (an empty collection exported) imports nothing and does not fail ; with **replace the collections**, the collection is made empty. | Database | Options of an import | |---|---| | MongoDB | replace the collections (`--drop`), without the indexes, in the order of the file, stop at the first error, without the validators, the users and roles of the file, keep the UUIDs of the collections ; for JSON and CSV : insert, upsert, merge or delete by the keys you name, without empty fields of CSV ; writers per collection | | PostgreSQL | create or replace the database, remove the objects first, without owners, without grants, schema only, data only, without triggers while the rows go in, all or nothing (one transaction), stop at the first error, connections in parallel, the roles of the file | | MySQL, MariaDB | create or replace the database, go on past errors (`--force`), the character set of a new database, the users and their grants | | SQL Server | create or replace the database, stop at the first error of a script, the time a statement may take | | Oracle | tables there already (skip, append the rows, empty them first, replace them), objects and rows, rows only or objects only, without creating the user, the directory object | > **What each database takes** > > - **PostgreSQL** : pg_restore reads a dump of its own version or older ; the job runs a pg_restore at least as new as the dump and the server. Some tables of a dump come with their keys, indexes, sequences, defaults, triggers and grants. > - **SQL Server** : SqlPackage imports a `.bacpac` into a new or empty database only — choose **Replace the database** to import over one which holds tables. A script and a `.bacpac` go in whole. > - **Oracle** : the dump goes through a directory object of the database (`DATA_PUMP_DIR` by default), which the user of the instance needs to read and write. A schema can go into another schema, a table under another name. **One moment** reads the change number of now with `DBMS_FLASHBACK` : a user without `EXECUTE` on it exports without one moment (`GRANT EXECUTE ON SYS.DBMS_FLASHBACK TO `). > - **MySQL and MariaDB** read each other's scripts. ## The file of an export ```text shop-pg-shop-20261031-012233.zip ├── manifest.json what the file holds : the kind of database, its version, the instance, the scope, │ the program, the databases and tables, the size and SHA-256 of each entry ├── README.txt how to restore it by hand, with the commands of the program └── shop.dump the file of the program : .archive.gz, .dump, .sql.gz, .bacpac, .dmp, .json, .csv ``` The SHA-256 of each entry is checked when the file is imported. The name says the instance, the database and the time of the export, in UTC. ## Jobs The **Jobs** tab lists the exports and imports of the instance, newest first : what each covers, its state, the size of its file and until when it stays (or **removed**), when and where it ran, and how long it took. On each row : **Download** and **Send to a storage** while the file of an export is there, the **log**, **Stop** a job which runs, **Remove** a job with its files and log. Tick some jobs and **Remove** them at once, or **Clear all** : every job of the instance which does not run. A job which runs is stopped first ; it can not be ticked. - An export runs on the server which started it, an import of an export where its file is ; the files stay on that server, and the other servers fetch them from it when they serve a download. - Two jobs run at a time on each server ; the others wait in the queue. - The files of a job are removed from the server **24 hours** after it ended : change it on Sandbox Settings, from 1 to 720 hours. The job and its log stay 30 days (or as long as the files, when that is longer), unless you remove them sooner. An import which was never started goes whole after the same hours. - A job which was running when its server stopped is marked failed when the server starts again. - The restores of [Automatic Backup](https://docs.apimaker.dev/v4/docs/features/automatic-backups.md#a-restore) are imports too : they are listed here, and in the log of Automatic Backup. ## The tools on Sandbox Settings `Operations → Sandbox Settings`, tab **Import & Export Tools** (`?am_sandbox_tab=tools`) : one row per tool. | Tool | Publisher | Downloaded from | |---|---|---| | MongoDB Database Tools | MongoDB, Inc. | downloads.mongodb.org : macOS, Windows, and the build for the Linux of the server (Ubuntu, Debian, Red Hat and its kin, Amazon Linux, SUSE) | | PostgreSQL client | The PostgreSQL Global Development Group | the PostgreSQL apt repository (Debian, Ubuntu), EnterpriseDB (macOS, Windows) ; on other Linux, the packages of the system | | MySQL client | Oracle Corporation | cdn.mysql.com — 8.4 LTS is recommended : the clients of MySQL 9 and later no longer log in with `mysql_native_password` | | MariaDB client | MariaDB Foundation | downloads.mariadb.org (Linux x86-64, Windows) | | SqlPackage | Microsoft | the releases of SqlPackage (x86-64) | - **Your versions** : the versions every server installs for your account, checked against the checksum the publisher gives. A chip says where the servers stand : ready, installing (with its share), or failed (with the error, and a button to try again). Up to 6 versions of each tool. - **The jobs run** : **Automatic** takes the version most compatible with the server of each database : its own line, from this server or its PATH, else installed from the publisher then ; else the nearest version which suits. Or choose one version. | Tool | The most compatible | |---|---| | PostgreSQL client | the major version of the server (pg_dump 16 for PostgreSQL 16) : a dump loads into a server of its version or newer ; else the nearest newer one | | MySQL client | the 8.4 client for MySQL 8 (the clients of 9 and later no longer log in with `mysql_native_password`), the line of the server for MySQL 9 and later | | MariaDB client | the line of the server (mariadb-dump 11.4 for MariaDB 11.4) : a newer one begins its scripts with what older servers refuse | | MongoDB Database Tools, SqlPackage | the newest : they read every version of their servers | - **This server** : first the version it runs now, which it finds by itself — ✓ with its version, *yours*, *installed by a job*, *listed by another admin* or *on the PATH* ; *installed from* the publisher *on first use* when nothing is there. Then the program its PATH finds and the versions it has besides yours. The page looks again each time the tab is shown, when you come back to the browser tab, and with **⟳** in the title of the card : a version a job just installed shows at once. Each admin has their own versions ; a version stays on a server while one admin lists it or one job runs on it. - **Automatic** names the version this server runs now (`Automatic · 8.4.13`) : the one a job runs when its database needs no other version. - **Files of the jobs stay** : how many hours the files of an export or an import stay after the job ended ; then only the job and its log. Where a publisher has no build for a server — MariaDB on macOS and on Linux ARM, SqlPackage on Linux ARM, MySQL and MariaDB on Windows ARM — the row says so, and the jobs use the programs the PATH of the server finds. The tools are in `__import_export_tools` next to the other folders of the installation, the files of the jobs in `__import_export_files`. A server without internet uses the programs on its PATH : install them with the package manager of the server. > **Warning : The files hold your data** > > An export holds every row of what it covers. The download link opens without signing in for 15 minutes : share it only with who may read the data. The passwords of the instances never go on a command line — the programs read them from files only the server's user can read, removed when the job ends — and they are hidden in the logs. ## Related - [Table designer](https://docs.apimaker.dev/v4/docs/schema/table-designer.md) : design tables, run a SQL script, drop a table. - [Database migration scripts](https://docs.apimaker.dev/v4/docs/features/database-migration.kotlin.md) : change the structure of the databases in code, once per environment. - [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.md) : the page of the tools. --- # 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.md). > **In short** > > - A website of API Maker is a folder of pages written in TSX, rendered by the server next to your data. > - The browser gets plain HTML : fast, and easy for search engines to read. > - You write it and see its preview in the admin panel. [![The workspace of a website : its files, the editor and the live preview](https://docs.apimaker.dev/assets/images/screens/ref-ssr.webp)](https://docs.apimaker.dev/assets/images/screens/ref-ssr.webp) 1. The files of the website. 2. The page, in TSX. 3. The preview, live. | | | |---|---| | 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.md). | | 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.md). | | 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.md). | | 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.md). | | Forms | An `action` takes a form ; every form carries a CSRF token by itself. [Forms](https://docs.apimaker.dev/v4/docs/ssr/forms.md). | | 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.md). | | Browser | Bootstrap, Tailwind, Chart.js, htmx… from a catalog with their hashes. [UI libraries](https://docs.apimaker.dev/v4/docs/ssr/ui-libraries.md). | | Git | `src/ssr//` of the repository of the account : commit, pull, revert. [Git](https://docs.apimaker.dev/v4/docs/ssr/git.md). | - Video: [Server-Side Rendered Websites with API Maker: First Steps in TSX](https://apimaker.dev/videos/ssr-first-steps) (6 min) ## 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.md#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.md). --- # 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.md)), 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.md). | | `auth.providers` | `[]` | The auth providers of the account the visitors sign in with. See [authentication](https://docs.apimaker.dev/v4/docs/ssr/authentication.md). | | `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.md#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.md). | | `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.md#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.md). | | **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. `apiPath` is the path of its account, as in the addresses of its APIs. So the website `shop` of the account `acme` answers at `https://api.example.com/ssr/acme/shop/products`. A name is unique in its account : two accounts may each have a `shop`. To show a website at the root of its own domain (`https://shop.example.com/products`), put a reverse proxy in front. ## With Launchpad Launchpad does all of this from a form, on Caddy, Ferron, Pingap and the [built-in web server](https://docs.apimaker.dev/v4/docs/features/built-in-web-server.md) of API Maker, and nothing in the code of the website changes. On the built-in web server the website needs no address at all : the API Maker it is part of shows it, and no request goes anywhere. 1. Open **Sites** of the server, then **Add a site**, then **SSR website**. 2. Paste the address of the website : `http://172.236.185.242:8080/ssr/admin/starter/`. 3. Choose how it is reached : a port (`http://172.236.185.242:8090`), or domains with HTTPS. Launchpad gets their certificates and renews them. 4. **Save and apply**. A website of the environment of the server needs no other address, and follows API Maker when it moves. Any other address works too : another server, another installation. The address on the server keeps working. A link people kept to it goes to the same page at the new address. ## 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. ## Any address, with no setting A reverse proxy can also say where it shows the website, for each request. Nothing is listed in `site.config.ts`, so an IP address and a port work too, and so does a proxy behind another proxy. This is what Launchpad does. | Header | What it says | |---|---| | `X-AM-SSR-Host` | The host the visitor asked for : `shop.example.com`, or `172.236.185.242:8090`. With it, the addresses of the website have no `/ssr/acme/shop` | | `X-AM-SSR-Proto` | `http` or `https` | | `X-AM-SSR-Port` | The port, for a proxy whose host has none. A port in `X-AM-SSR-Host` wins | The proxy must write these headers itself and not pass on what a visitor sends. A host that is not valid is ignored, and a page never sees these headers. ```caddy http://:8090 { rewrite * /ssr/acme/shop{uri} reverse_proxy 127.0.0.1:48246 { header_up X-AM-SSR-Host {hostport} header_up X-AM-SSR-Proto {scheme} header_up -X-AM-SSR-Port } } ``` ## HTTPS The [built-in web server](https://docs.apimaker.dev/v4/docs/features/built-in-web-server.md) of API Maker shows a website at the root of a domain with HTTPS by itself : a site of the type `ssr` (`{ "type": "ssr", "domains": ["shop.example.com"], "account": "admin", "website": "starter" }`), certificates included. Behind another proxy, 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.md#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.md), 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.md) 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.md#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.md) have many more, with comments. - Video: [A Real Online Store on Four Databases, Rendered by API Maker](https://apimaker.dev/videos/ssr-storefront) (6 min) ## 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.md). | ## 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 `