# 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<JsonObject>), mapOf and data classes as JSON, AmErrors, packages from Maven Central, with many examples.

Source: https://docs.apimaker.dev/v4/docs/languages/kotlin.html

Code in Kotlin defines `fun main(g: G): Any?` at the top of its file. It is compiled once by the compiler of Kotlin, and runs on the image of the JDK of Eclipse Temurin (`eclipse-temurin:25-jdk` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) name another version), with only the packages you add from Maven Central.

**hello.kt**

```kotlin
import am.G

fun main(g: G): Any? {
    return mapOf("hello" to g.req.body.getString("name", "world"))
}
```

| | |
|---|---|
| Runtime | Kotlin on the JDK, 20 or later : a version of `eclipse-temurin:<version>-jdk`. The version of the settings is the one of the JDK : `25` (default), `21` |
| Compiler | Kotlin `2.4.20`, or the one the [Dockerfile](https://docs.apimaker.dev/v4/docs/languages/kotlin.html#the-compiler) names |
| `main` | a function `main(g: G)` at the top of the file : it returns `Any?`, any other type, or nothing |
| Answer | what `main` returns, written as JSON; `null` lets the API go on |
| Calls of `g.sys` | typed : what a call takes is a class made key after key (`GetAll("mysql_8", "inventory", "customers").limit(10)`), what it answers has its type (`List<JsonObject>`, `JsonObject?`, `Long`, `UpdateManyResult`) |
| Values | plain JSON : an object is an `am.JsonObject` (a `MutableMap<String, Any?>` 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<JsonObject>`, 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<List<JsonObject>>` (`isSuccess`, `data`, `totalCount`, `errors`). The thread of the request waits for the answer; the other requests go on.

**Database calls**

```kotlin
import am.Count
import am.G
import am.GetAll
import am.GetById

fun main(g: G): Any? {
    val rows = g.sys.db.getAll(GetAll("mysql_8", "inventory", "customers").limit(10).sort("-customer_id"))
    val two = g.sys.db.getById(GetById("mysql_8", "inventory", "customers", 2))
    val count = g.sys.db.count(Count("mysql_8", "inventory", "customers").find(mapOf("country" to "IN")))
    return mapOf("first" to rows.firstOrNull(), "name" to two?.getString("first_name"), "inIndia" to count)
}
```

**Save, update, remove**

```kotlin
import am.G
import am.RemoveById
import am.Save
import am.UpdateById

fun main(g: G): Any? {
    val saved = g.sys.db.saveSingleOrMultiple(Save("mysql_8", "inventory", "customers", mapOf("first_name" to "Ada", "last_name" to "Lovelace")))
    g.sys.db.updateById(UpdateById("mysql_8", "inventory", "customers", 2).updateData(mapOf("first_name" to "MALLORY")))
    g.sys.db.removeById(RemoveById("mysql_8", "inventory", "customers", 101))
    // What a save answers : saved.row(), saved.rows(), saved.isList.
    return saved.row()
}
```

Calls at the same time are threads. Virtual threads (JDK 21 and later) cost next to nothing :

**Calls at the same time**

```kotlin
import am.Count
import am.G
import java.util.concurrent.Callable
import java.util.concurrent.Executors

fun main(g: G): Any? = Executors.newVirtualThreadPerTaskExecutor().use { threads ->
    val orders = threads.submit(Callable { g.sys.db.count(Count("mongodb", "shop", "orders")) })
    val products = threads.submit(Callable { g.sys.db.count(Count("mongodb", "shop", "products")) })
    mapOf("orders" to orders.get(), "products" to products.get())
}
```

- A call which fails in a thread comes out of `get()` : its errors are the errors of the answer.
- Coroutines are a package, not a part of the language : add `org.jetbrains.kotlinx:kotlinx-coroutines-core` to the [packages](https://docs.apimaker.dev/v4/docs/languages/kotlin.html#packages) of Kotlin to use `runBlocking`, `async` and `await`.

### Rows as they arrive

**Sum a large table**

```kotlin
import am.G
import am.GetAll

fun main(g: G): Any? {
    var total = 0.0
    g.sys.db.getAllByStream(GetAll("mysql_8", "inventory", "orders")) { row ->
        total += row.getDouble("amount")
    }
    return mapOf("amount" to total)
}
```

The function of the rows changes your variables as any lambda of Kotlin does. What it throws ends the call.

### System APIs and cache

**Encrypt, hash, events and cache keys**

```kotlin
import am.G

fun main(g: G): Any? {
    val sealedData = g.sys.system.encrypt(mapOf("card" to "4111"))
    val digest = g.sys.system.hash("my text")
    g.sys.system.emitEvent("order_created", mapOf("id" to 7), listOf("notify_shop"))
    g.sys.cache.setKey("last_order", "7", 600)
    return mapOf("sealedData" to sealedData, "digest" to digest, "last" to g.sys.cache.getKey("last_order"))
}
```

## Errors

**Refuse a request**

```kotlin
import am.AmErrors
import am.G

fun main(g: G): Any? {
    if (!g.req.body.has("quantity")) throw AmErrors("'quantity' is missing.", 400)
    return mapOf("ok" to true)
}
```

**Catch the errors of a call**

```kotlin
import am.AmErrors
import am.G
import am.RemoveById

fun main(g: G): Any? {
    try {
        g.sys.db.removeById(RemoveById("mysql_8", "inventory", "customers", 404))
        return mapOf("removed" to true)
    } catch (refused: AmErrors) {
        g.logger.warn("not removed :", refused.message)
        return mapOf("removed" to false, "code" to refused.code)
    }
}
```

- `refused.errors` is the list of the errors (each a `ResponseError` : `message`, `code`, `field`…), `refused.code` the code of the first one. Several errors at once : `throw AmErrors(listOf(mapOf("code" to 400, "message" to "…"), …))`.
- Any other exception answers `500` with its type and message, the message of what it happened because of, and the lines of your code : `at OrdersKt.helper(orders.kt:8)`.

## Logs, printing and shared values

**Logs and shared values**

```kotlin
import am.G

fun main(g: G): Any? {
    g.logger.log("order", 7, "paid")                 // "order 7 paid"
    g.logger.error(mapOf("step" to "payment"))       // maps, lists and data classes as indented JSON
    println("kept in the console of the account")
    // Lives as long as the runner, shared by every piece of code it runs.
    return g.shared.merge("calls", 1) { old, _ -> (old as Int) + 1 }
}
```

## Hooks

**Pre hook : only my rows**

```kotlin
import am.G

fun main(g: G): Any? {
    // objectAt : the object at a key, made and kept there when none is.
    g.req.query.objectAt("find")["owner_id"] = g.req.auth.getObject("authAMDB")["id"]
    return null
}
```

**Post hook : hide a column**

```kotlin
import am.G

fun main(g: G): Any? {
    for (row in g.res.output as? List<*> ?: emptyList<Any?>()) {
        (row as? MutableMap<*, *>)?.remove("password")
    }
    return null
}
```

## Utility classes

A [utility class](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) written in Kotlin starts as an `object` of its name. The utility classes of Kotlin of an account are compiled together, and the other Kotlin code of the account names what they declare, without an import.

**The utility class Tax**

```kotlin
object Tax {
    const val RATE = 0.2

    fun add(amount: Double): Double = Round.cents(amount * (1 + RATE))   // Round : another utility class
}
```

**A custom API which uses it**

```kotlin
import am.G

fun main(g: G): Any? = mapOf("total" to Tax.add(g.req.body.getDouble("amount")))
```

- What else the file declares at its top (functions, classes, constants) is there for the code as well : `fun addTax(amount: Double)` of the class is `addTax(100.0)` in the code.
- The name of the utility class : letters of a to z, digits and `_`, no digit first, no word of Kotlin (`object`, `when`…) and not `am`. One class of a name per account, whatever its folder : the classes are in no package, so write no `package` line.
- The classes are loaded once per runner and shared by all Kotlin code of the account there : what an `object` keeps is kept there. A class which is saved again is compiled anew with the others, and the code which uses them is compiled again on its next run.
- What the code declares comes before what a utility class declares under the same name.
- What does not compile in a class is said with its file and its line : `src/utility_classes/utils---Tax/Tax.kt:3:16: error: unresolved reference 'missing'.`
- In the Git repository the class is `src/utility_classes/<folder>---<name>/<name>.kt` : the project of Gradle compiles the classes together, then every piece of code with them.

## Test cases

A [test case](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) written in Kotlin has a function per test : every function at the top of the file whose name starts with `test` and which takes `g: G`. What it throws is its failure.

**A test case**

```kotlin
import am.G
import am.JsonObject
import am.Mock
import am.RunCustomApi

fun testTheCostOfAParcel(g: G) {
    g.sys.test.mock(Mock("db.getAll").whenever(mapOf("collection" to "rates")).returns(listOf(mapOf("per_kg" to 2.5))))
    val output = JsonObject.from(g.sys.test.runCustomApi(RunCustomApi("shippingCost").body(mapOf("weight" to 4))))
    check(output.getDouble("cost") == 10.0) { "the cost is " + output["cost"] }
}
```

- `g.sys.test` : `runCustomApi(RunCustomApi(…))`, `runCustomApiFull(…)` for the whole answer (`data`, `statusCode`, `headers`, `logs`), `mock(Mock(…))`, `mocks(listOf(…))`, `clearMocks()` or `clearMocks(method)`, `calls()` or `calls(method)`. The key `when` of a mock is set by `whenever` : `when` is a word of Kotlin.
- The tests run in the order they are written. The custom API under test can be written in any language.
- `check(…) { … }` and `require(…) { … }` always check. `assert(…)` is checked only by a JVM started with `-ea`, and the runner is not.
- The lines of a test run are counted on JDK 24 and later. On an older JDK the tests run as well, and the coverage has no lines of Kotlin.

## Packages

Add `com.google.code.gson:gson` in [Sandbox Settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages), then :

**A package of Maven Central**

```kotlin
import am.G
import com.google.gson.Gson

fun main(g: G): Any? = mapOf("json" to Gson().toJson(listOf(1, 2)))
```

- A package is named by its group and its artifact : `group:artifact`. Versions are the ones of Maven : `2.11.0`, `33.4.0-jre`, a range `[1.0,2.0)`; empty is the latest release.
- Maven resolves the packages, with what they need, when the image is built; while code runs nothing is downloaded. Code which imports what is not there is told so : `'com.google.gson.Gson' is not there : is its package in the packages of Kotlin in the settings of the sandbox?`.
- The standard library of Kotlin is always there. Coroutines (`org.jetbrains.kotlinx:kotlinx-coroutines-core`), serialization and reflection are packages.
- [Java](https://docs.apimaker.dev/v4/docs/languages/java.html) has packages of its own, from Maven Central too : a package added to Kotlin is not there for Java code.

### The compiler

The image gets the compiler of Kotlin `2.4.20` from Maven Central when it is built. Another version is a line of the [Dockerfile](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#dockerfile) of Kotlin, above the packages :

**The Dockerfile of Kotlin with another compiler**

```dockerfile
FROM {{AM_BASE_IMAGE}}

ENV AM_KOTLIN_VERSION=2.3.0

{{AM_PACKAGES}}

{{AM_RUNNER}}
```

## How it runs

- Each piece of code is compiled once, with its lines : stack traces say them. Saving starts that compilation in the background, so the first request does not wait for it, and code which does not compile is reported in the console of the account (`main.kt:4:12: error: unresolved reference 'nothing'.`).
- Every request runs on a thread of its own; requests run at the same time, on every core.
- Variables at the top of your file live as long as the runner, per piece of code : protect them (`AtomicLong`, `@Synchronized`), or use `g.shared`.
- Each piece of code is compiled and loaded on its own : two pieces of code can declare functions and classes of the same names, and see only the utility classes of the account.
- The code is compiled for the JVM 17 and runs on the JDK of the settings : what that JDK has, the code can use.

## In the code editor

The editor completes `g` as Kotlin writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in [the code editor in every language](https://docs.apimaker.dev/v4/docs/languages/index.html#editor).

**Where the editor helps**

```kotlin
import am.G
import am.GetAll

fun main(g: G): Any? {
    // g.  →  req, res, sys, logger, shared      g.sys.db.  →  getAll, getById, query, gen, full…
    g.logger.log(g.req.body)  // g.req.  →  body, query, headers, auth…      g.res.  →  statusCode, headers…
    val rows = g.sys.db.getAll(GetAll("mysql_8", "inventory", "customers").limit(10))
    //         GetAll("  →  your instances, then its databases, then their tables
    //         GetAll(…).  →  find, limit, sort, select…
    for (row in rows) {
        g.logger.log(row.getString("first_name"))  // row.getString("  →  the columns of customers
    }
    return rows[0].getString("first_name")
}
```

- After `GetAll(…).` the editor offers the methods which set its keys, each with what it is for ; a method gives the same value, so the next key follows. Inside `.find(mapOf("` it offers the columns of the table, inside `.select("` and `.sort("` too.
- What a call answers has its type : on the rows of a `getAll` the columns are offered inside the quotes of `getString`, `getLong`, `getDouble`, `getObject`… and of an index (`row["`), in a `for` loop, in `forEach { row -> … }` and in the function given to `getAllByStream`, also under its name `it`. `changed.` (an `updateMany`) offers `updatedRowsCount`, `saved.` offers `row`, `rows` and `isList`, the whole answer of `g.sys.db.full` offers `isSuccess`, `data`, `errors`…
- `g.req.` and `g.res.` offer a property for each key of the request and of the answer ; `g.req.auth.` the opened tokens, written as an index (`g.req.auth["authAMUser"]`). A value your code says is an object has its keys and what a `JsonObject` has : `JsonObject.from(value).`, `(value as JsonObject).`.

## Reference

| `g.` | Kotlin |
|---|---|
| `req` | `am.Request` : `body`, `query`, `params`, `headers`, `auth`, `reqInfo` (each a `JsonObject`), `eventData`, `isApiRequestFromUser` ; a `JsonObject` itself |
| `res` | `am.Response` : `statusCode`, `contentType`, `headers`, `shared`, `output`, `errors`, `warnings` ; a `JsonObject` itself |
| `sys.db` | `getAll(GetAll)`, `getById(GetById)`, `query(Query)`, `saveSingleOrMultiple(Save)`, `masterSave(Save)`, `arrayOperations(ArrayOperations)`, `updateById(UpdateById)`, `updateMany(UpdateMany)`, `replaceById(UpdateById)`, `removeById(RemoveById)`, `removeByQuery(RemoveByQuery)`, `aggregate(Aggregate)`, `count(Count)`, `distinct(Distinct)`, `distinctQuery(Distinct)`, `getAllByStream`, `queryByStream` ; `sys.db.gen.getAllGen`… |
| `sys.db.full`, `sys.db.gen.full`, `sys.system.full`, `sys.cache.full` | the same calls, each with its whole answer : `ApiResponse<T>` (`isSuccess`, `statusCode`, `data`, `totalCount`, `errors`, `warnings`) |
| `sys.system` | `encrypt`, `encryptWith`, `decrypt`, `decryptWith`, `hash`, `getSecret`, `getSecrets`, `callExternalApi`, `callExternalApis`, `getToken`, `getTokens`, `executeQuery`, `getTableMeta`, `getTableMetas`, `createIndexes`, `dropIndexes`, `dropIndexesOfMany`, `getIndexes`, `getIndexesOfMany`, `emitEvent`, `emitEventWS`, `isValidDataForTable(s)`, `isValidDataForCustomAPI(s)`, `multiTenantInstance(s)Updated`, `isValidConnectionString(s)` |
| `sys.cache` | `getKey`, `getKeys`, `setKey`, `setKeys`, `removeKey`, `removeKeys`, `resetCacheDB`, `resetCacheDBs`, `resetCacheCustomApis`, `resetCacheCustomApisOfTenant`, `resetCacheSystemApis`, `resetCacheSystemApisOfTenant` |
| `sys.call("db.getAll", …)` | any API by its name in JavaScript, with its arguments as they are |
| `JsonObject` | `of`, `from`, `getString`, `getLong`, `getInt`, `getDouble`, `getBoolean`, `getObject`, `objectAt`, `getList`, `getObjects`, `getStrings`, `has`, `with`, `at` |
| `logger` | `log`, `debug`, `info`, `warn`, `error` |
| `shared` | a `ConcurrentHashMap<String, Any>` 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<ResponseError>`), `.code` |

## Related

- [Java](https://docs.apimaker.dev/v4/docs/languages/java.html) · [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html)
