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

The code in every language

Each example is the inside of main, written in every language API Maker runs : pick yours in the tabs, and every page shows it from then on. The types a call takes (GetAll, DeepItem…) are those of am : from am import GetAll in Python, use am::GetAll; in Rust, import am.GetAll; in Java and Kotlin ; PHP and C# have them without an import, Go names them am.GetAll.

Custom API examples

The code of a custom API : what to return, how to call the APIs of API Maker, how to send files.

Basic code

  • A custom API is the function main of its language : what it returns is the data of the answer. The pieces below are the inside of main.
1
2
3
return {
    hello: 'world'
};
return {'hello': 'world'}
return ['hello' => 'world'];
return new { hello = "world" };
return am.M{"hello": "world"}, nil
Ok(json!({ "hello": "world" }))
return Map.of("hello", "world");
return mapOf("hello" to "world")

Calls of g.sys

  • In TypeScript every g.sys call returns a promise : await it. A call without await runs, but its result and its errors are lost. The same in Python (in async def main) and C#. PHP, Go, Rust, Java and Kotlin wait for the answer of each call.
1
2
3
return await g.sys.db.getAll({
    instance: "INSTANCE_NAME", database: 'DB_NAME', collection: "COLLECTION_NAME",
});
return await g.sys.db.get_all('INSTANCE_NAME', 'DB_NAME', 'COLLECTION_NAME')
return $g->sys->db->getAll('INSTANCE_NAME', 'DB_NAME', 'COLLECTION_NAME');
return await g.Sys.Db.GetAll(new GetAll("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME"));
return g.Sys.Db.GetAll(am.GetAll{Instance: "INSTANCE_NAME", Database: "DB_NAME", Collection: "COLLECTION_NAME"})
Ok(json!(g.sys.db.get_all(GetAll::new("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME"))?))
return g.sys.db.getAll(new GetAll("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME"));
return g.sys.db.getAll(GetAll("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME"))

The whole envelope : true, or full

  • A g.sys call returns the data and throws the errors of the API.
  • With true as the second argument in TypeScript, or through full in the other languages (g.sys.db.full, g.Sys.Db.Full), it returns the whole envelope { success, statusCode, data, errors, warnings } and never throws : read success yourself.
1
2
3
4
5
6
7
8
9
let saveData = await g.sys.db.saveSingleOrMultiple({
    instance: "INSTANCE_NAME", database: 'DB_NAME', collection: "COLLECTION_NAME",
    saveData: {},
    headers: {}
}, true);
let countOfAllData = await g.sys.db.count({
    instance: "INSTANCE_NAME", database: 'DB_NAME', collection: "COLLECTION_NAME",
}, true);
return { saveData, countOfAllData };
1
2
3
save_data = await g.sys.db.full.save_single_or_multiple('INSTANCE_NAME', 'DB_NAME', 'COLLECTION_NAME', {}, headers={})
count_of_all_data = await g.sys.db.full.count('INSTANCE_NAME', 'DB_NAME', 'COLLECTION_NAME')
return {'saveData': save_data, 'countOfAllData': count_of_all_data}
1
2
3
$saveData = $g->sys->db->full->saveSingleOrMultiple('INSTANCE_NAME', 'DB_NAME', 'COLLECTION_NAME', new AmObject(), headers: new AmObject());
$countOfAllData = $g->sys->db->full->count('INSTANCE_NAME', 'DB_NAME', 'COLLECTION_NAME');
return ['saveData' => $saveData, 'countOfAllData' => $countOfAllData];
1
2
3
var saveData = await g.Sys.Db.Full.SaveSingleOrMultiple(new Save("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME", new JsonObject()) { Headers = new JsonObject() });
var countOfAllData = await g.Sys.Db.Full.Count(new Count("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME"));
return new { saveData, countOfAllData };
1
2
3
4
5
6
7
8
9
saveData, err := g.Sys.Db.Full.SaveSingleOrMultiple(am.Save{Instance: "INSTANCE_NAME", Database: "DB_NAME", Collection: "COLLECTION_NAME", SaveData: am.M{}, Headers: am.M{}})
if err != nil {
    return nil, err
}
countOfAllData, err := g.Sys.Db.Full.Count(am.Count{Instance: "INSTANCE_NAME", Database: "DB_NAME", Collection: "COLLECTION_NAME"})
if err != nil {
    return nil, err
}
return am.M{"saveData": saveData, "countOfAllData": countOfAllData}, nil
1
2
3
let save_data = g.sys.db.full.save_single_or_multiple(Save::new("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME", json!({})).headers(json!({})))?;
let count_of_all_data = g.sys.db.full.count(Count::new("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME"))?;
Ok(json!({ "saveData": { "success": save_data.success, "data": save_data.data.row() }, "countOfAllData": { "success": count_of_all_data.success, "data": count_of_all_data.data } }))
1
2
3
ApiResponse<Saved> saveData = g.sys.db.full.saveSingleOrMultiple(new Save("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME", Map.of()).headers(Map.of()));
ApiResponse<Long> countOfAllData = g.sys.db.full.count(new Count("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME"));
return Map.of("saveData", saveData, "countOfAllData", countOfAllData);
1
2
3
val saveData = g.sys.db.full.saveSingleOrMultiple(Save("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME", mapOf<String, Any?>()).headers(mapOf<String, Any?>()))
val countOfAllData = g.sys.db.full.count(Count("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME"))
return mapOf("saveData" to saveData, "countOfAllData" to countOfAllData)

Throw an error

  • An error raised ends the call with success: false, the message in errors and its code as the status. A message listed in the errorList of the API can be translated.
1
2
3
4
5
6
7
let saveData = await g.sys.db.saveSingleOrMultiple({
    instance: "INSTANCE_NAME", database: 'DB_NAME', collection: "COLLECTION_NAME",
    saveData: {},
    headers: {}
}, true);
if (!saveData.success) throw [{ code: T.EStatusCode.BAD_REQUEST, message: 'Your data is not saved!' }];
return saveData;
1
2
3
4
save_data = await g.sys.db.full.save_single_or_multiple('INSTANCE_NAME', 'DB_NAME', 'COLLECTION_NAME', {}, headers={})
if not save_data.success:
    raise Errors('Your data is not saved!', code=400)
return save_data
1
2
3
4
5
$saveData = $g->sys->db->full->saveSingleOrMultiple('INSTANCE_NAME', 'DB_NAME', 'COLLECTION_NAME', new AmObject(), headers: new AmObject());
if (!$saveData->success) {
    throw new AmErrors('Your data is not saved!', 400);
}
return $saveData;
1
2
3
var saveData = await g.Sys.Db.Full.SaveSingleOrMultiple(new Save("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME", new JsonObject()) { Headers = new JsonObject() });
if (!saveData.Success) throw new AmErrors("Your data is not saved!", 400);
return saveData;
1
2
3
4
5
6
7
8
saveData, err := g.Sys.Db.Full.SaveSingleOrMultiple(am.Save{Instance: "INSTANCE_NAME", Database: "DB_NAME", Collection: "COLLECTION_NAME", SaveData: am.M{}, Headers: am.M{}})
if err != nil {
    return nil, err
}
if !saveData.Success {
    return nil, am.Fail(400, "Your data is not saved!")
}
return saveData, nil
1
2
3
4
5
let save_data = g.sys.db.full.save_single_or_multiple(Save::new("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME", json!({})).headers(json!({})))?;
if !save_data.success {
    return Err(am::fail(400, "Your data is not saved!"));
}
Ok(json!({ "success": save_data.success, "data": save_data.data.row() }))
1
2
3
ApiResponse<Saved> saveData = g.sys.db.full.saveSingleOrMultiple(new Save("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME", Map.of()).headers(Map.of()));
if (!saveData.isSuccess()) throw new AmErrors("Your data is not saved!", 400);
return saveData;
1
2
3
val saveData = g.sys.db.full.saveSingleOrMultiple(Save("INSTANCE_NAME", "DB_NAME", "COLLECTION_NAME", mapOf<String, Any?>()).headers(mapOf<String, Any?>()))
if (!saveData.isSuccess) throw AmErrors("Your data is not saved!", 400)
return saveData

Return an image or any binary

  • Set g.res.contentType and return the base64 of the content.
g.res.contentType = 'image/gif' as T.EContentType;
return `data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=`
g.res.content_type = 'image/gif'
return 'data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA='
$g->res->contentType = 'image/gif';
return 'data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=';
g.Res.ContentType = "image/gif";
return "data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=";
g.Res.SetContentType("image/gif")
return "data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=", nil
g.res.set_content_type("image/gif");
Ok(json!("data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA="))
g.res.setContentType("image/gif");
return "data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=";
g.res.contentType = "image/gif"
return "data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA="

Download file

import * as T from 'types';
import * as fs from 'fs';
import * as path from 'path';
async function main(g: T.IAMGlobal) {
    // always write file in the uploads folder which you want to send to user.
    // you can write other files in any directory which you don't want to send to user.
    await fs.promises.mkdir(path.join(__dirname, 'uploads'), { recursive: true });
    let filePath = path.join(__dirname, 'uploads', 'myfile.txt');
    await fs.promises.writeFile(filePath, 'file 1 content', { encoding: 'utf8' });
    return {
        // just provide file name which is written to "uploads" directory
        __am__downloadFilePath: 'myfile.txt',
        __am__downloadFolderFileName: 'newName.txt', // download file will have this name.
    }
};
module.exports = main;
import os

from am import G


async def main(g: G):
    # Write the files to send in the folder uploads ; the other files anywhere else.
    os.makedirs('uploads', exist_ok=True)
    with open(os.path.join('uploads', 'myfile.txt'), 'w', encoding='utf-8') as file:
        file.write('file 1 content')
    return {
        '__am__downloadFilePath': 'myfile.txt',          # its name in the folder uploads
        '__am__downloadFolderFileName': 'newName.txt',   # the name the file is saved under
    }
<?php
function main(AmG $g)
{
    // Write the files to send in the folder uploads ; the other files anywhere else.
    if (!is_dir('uploads')) {
        mkdir('uploads', 0777, true);
    }
    file_put_contents('uploads/myfile.txt', 'file 1 content');
    return [
        '__am__downloadFilePath' => 'myfile.txt',          // its name in the folder uploads
        '__am__downloadFolderFileName' => 'newName.txt',   // the name the file is saved under
    ];
}
using System.IO;

file static class Api
{
    public static async Task<object?> Main(G g)
    {
        // Write the files to send in the folder uploads ; the other files anywhere else.
        Directory.CreateDirectory("uploads");
        await File.WriteAllTextAsync(Path.Combine("uploads", "myfile.txt"), "file 1 content");
        return new JsonObject
        {
            ["__am__downloadFilePath"] = "myfile.txt",          // its name in the folder uploads
            ["__am__downloadFolderFileName"] = "newName.txt",   // the name the file is saved under
        };
    }
}
package code

import (
    "os"
    "path/filepath"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    // Write the files to send in the folder uploads ; the other files anywhere else.
    if err := os.MkdirAll("uploads", 0o755); err != nil {
        return nil, err
    }
    if err := os.WriteFile(filepath.Join("uploads", "myfile.txt"), []byte("file 1 content"), 0o644); err != nil {
        return nil, err
    }
    return am.M{
        "__am__downloadFilePath":       "myfile.txt",  // its name in the folder uploads
        "__am__downloadFolderFileName": "newName.txt", // the name the file is saved under
    }, nil
}
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    // Write the files to send in the folder uploads ; the other files anywhere else.
    std::fs::create_dir_all("uploads")?;
    std::fs::write("uploads/myfile.txt", "file 1 content")?;
    Ok(json!({
        "__am__downloadFilePath": "myfile.txt",          // its name in the folder uploads
        "__am__downloadFolderFileName": "newName.txt",   // the name the file is saved under
    }))
}
import am.G;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

class Api {
    static Object main(G g) throws Exception {
        // Write the files to send in the folder uploads ; the other files anywhere else.
        Files.createDirectories(Path.of("uploads"));
        Files.writeString(Path.of("uploads", "myfile.txt"), "file 1 content");
        return Map.of(
            "__am__downloadFilePath", "myfile.txt",          // its name in the folder uploads
            "__am__downloadFolderFileName", "newName.txt");  // the name the file is saved under
    }
}
import am.G
import java.io.File

fun main(g: G): Any? {
    // Write the files to send in the folder uploads ; the other files anywhere else.
    File("uploads").mkdirs()
    File("uploads", "myfile.txt").writeText("file 1 content")
    return mapOf(
        "__am__downloadFilePath" to "myfile.txt",          // its name in the folder uploads
        "__am__downloadFolderFileName" to "newName.txt",   // the name the file is saved under
    )
}

Download a folder as a zip

  • A folder of uploads goes into the zip under the path archiveDestination.
import * as T from 'types';
import * as fs from 'fs';
import * as path from 'path';
async function main(g: T.IAMGlobal) {
    return {
        __am__downloadFileOrFolderPaths: [
            {
                fsSource: path.join('folder1', 'folder2', 'folder3', 'data_folder'), // path into uploads folder
                archiveDestination: 'data_folder', // path in zip file, so data_folder folder will be directly available in zip file.
            },
        ],
        __am__downloadFolderFileName: `output.zip`, // download file will have this name.
    }
};
module.exports = main;
from am import G


async def main(g: G):
    return {
        '__am__downloadFileOrFolderPaths': [
            {
                'fsSource': 'folder1/folder2/folder3/data_folder',   # a path in the folder uploads
                'archiveDestination': 'data_folder',                  # its path in the zip file
            },
        ],
        '__am__downloadFolderFileName': 'output.zip',                 # the name the file is saved under
    }
<?php
function main(AmG $g)
{
    return [
        '__am__downloadFileOrFolderPaths' => [
            [
                'fsSource' => 'folder1/folder2/folder3/data_folder',   // a path in the folder uploads
                'archiveDestination' => 'data_folder',                  // its path in the zip file
            ],
        ],
        '__am__downloadFolderFileName' => 'output.zip',                 // the name the file is saved under
    ];
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        return new JsonObject
        {
            ["__am__downloadFileOrFolderPaths"] = new JsonArray(new JsonObject
            {
                ["fsSource"] = "folder1/folder2/folder3/data_folder",   // a path in the folder uploads
                ["archiveDestination"] = "data_folder",                  // its path in the zip file
            }),
            ["__am__downloadFolderFileName"] = "output.zip",             // the name the file is saved under
        };
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    return am.M{
        "__am__downloadFileOrFolderPaths": []any{
            am.M{
                "fsSource":           "folder1/folder2/folder3/data_folder", // a path in the folder uploads
                "archiveDestination": "data_folder",                         // its path in the zip file
            },
        },
        "__am__downloadFolderFileName": "output.zip", // the name the file is saved under
    }, nil
}
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    Ok(json!({
        "__am__downloadFileOrFolderPaths": [
            {
                "fsSource": "folder1/folder2/folder3/data_folder",   // a path in the folder uploads
                "archiveDestination": "data_folder",                  // its path in the zip file
            },
        ],
        "__am__downloadFolderFileName": "output.zip",                 // the name the file is saved under
    }))
}
import am.G;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        return Map.of(
            "__am__downloadFileOrFolderPaths", List.of(Map.of(
                "fsSource", "folder1/folder2/folder3/data_folder",   // a path in the folder uploads
                "archiveDestination", "data_folder")),               // its path in the zip file
            "__am__downloadFolderFileName", "output.zip");           // the name the file is saved under
    }
}
import am.G

fun main(g: G): Any? {
    return mapOf(
        "__am__downloadFileOrFolderPaths" to listOf(
            mapOf(
                "fsSource" to "folder1/folder2/folder3/data_folder",   // a path in the folder uploads
                "archiveDestination" to "data_folder",                  // its path in the zip file
            ),
        ),
        "__am__downloadFolderFileName" to "output.zip",                 // the name the file is saved under
    )
}

Download files & folders

import * as T from 'types';
import * as fs from 'fs';
import * as path from 'path';
async function main(g: T.IAMGlobal) {
    // always write file in the uploads folder which you want to send to user.
    // you can write other files in any directory which you don't want to send to user.
    await fs.promises.mkdir(path.join(__dirname, 'uploads'), { recursive: true });
    let filePath = path.join(__dirname, 'uploads', 'myfile.txt');
    await fs.promises.writeFile(filePath, 'file 1 content', { encoding: 'utf8' });
    return {
        // You can provide list of files or folders, it will will make zip and send to user.
        __am__downloadFileOrFolderPaths: [
            'myfile.txt', // file name
            'someFolder', // folder name
        ],
        __am__downloadFolderFileName: 'zipFile.zip', // this will be zip file name
    }
};
module.exports = main;
import os

from am import G


async def main(g: G):
    # Write the files to send in the folder uploads ; the other files anywhere else.
    os.makedirs('uploads', exist_ok=True)
    with open(os.path.join('uploads', 'myfile.txt'), 'w', encoding='utf-8') as file:
        file.write('file 1 content')
    return {
        # Files and folders of the folder uploads : they go out as one zip file.
        '__am__downloadFileOrFolderPaths': ['myfile.txt', 'someFolder'],
        '__am__downloadFolderFileName': 'zipFile.zip',   # the name of the zip file
    }
<?php
function main(AmG $g)
{
    // Write the files to send in the folder uploads ; the other files anywhere else.
    if (!is_dir('uploads')) {
        mkdir('uploads', 0777, true);
    }
    file_put_contents('uploads/myfile.txt', 'file 1 content');
    return [
        // Files and folders of the folder uploads : they go out as one zip file.
        '__am__downloadFileOrFolderPaths' => ['myfile.txt', 'someFolder'],
        '__am__downloadFolderFileName' => 'zipFile.zip',   // the name of the zip file
    ];
}
using System.IO;

file static class Api
{
    public static async Task<object?> Main(G g)
    {
        // Write the files to send in the folder uploads ; the other files anywhere else.
        Directory.CreateDirectory("uploads");
        await File.WriteAllTextAsync(Path.Combine("uploads", "myfile.txt"), "file 1 content");
        return new JsonObject
        {
            // Files and folders of the folder uploads : they go out as one zip file.
            ["__am__downloadFileOrFolderPaths"] = new JsonArray("myfile.txt", "someFolder"),
            ["__am__downloadFolderFileName"] = "zipFile.zip",   // the name of the zip file
        };
    }
}
package code

import (
    "os"
    "path/filepath"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    // Write the files to send in the folder uploads ; the other files anywhere else.
    if err := os.MkdirAll("uploads", 0o755); err != nil {
        return nil, err
    }
    if err := os.WriteFile(filepath.Join("uploads", "myfile.txt"), []byte("file 1 content"), 0o644); err != nil {
        return nil, err
    }
    return am.M{
        // Files and folders of the folder uploads : they go out as one zip file.
        "__am__downloadFileOrFolderPaths": []string{"myfile.txt", "someFolder"},
        "__am__downloadFolderFileName":    "zipFile.zip", // the name of the zip file
    }, nil
}
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    // Write the files to send in the folder uploads ; the other files anywhere else.
    std::fs::create_dir_all("uploads")?;
    std::fs::write("uploads/myfile.txt", "file 1 content")?;
    Ok(json!({
        // Files and folders of the folder uploads : they go out as one zip file.
        "__am__downloadFileOrFolderPaths": ["myfile.txt", "someFolder"],
        "__am__downloadFolderFileName": "zipFile.zip",   // the name of the zip file
    }))
}
import am.G;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) throws Exception {
        // Write the files to send in the folder uploads ; the other files anywhere else.
        Files.createDirectories(Path.of("uploads"));
        Files.writeString(Path.of("uploads", "myfile.txt"), "file 1 content");
        return Map.of(
            // Files and folders of the folder uploads : they go out as one zip file.
            "__am__downloadFileOrFolderPaths", List.of("myfile.txt", "someFolder"),
            "__am__downloadFolderFileName", "zipFile.zip");   // the name of the zip file
    }
}
import am.G
import java.io.File

fun main(g: G): Any? {
    // Write the files to send in the folder uploads ; the other files anywhere else.
    File("uploads").mkdirs()
    File("uploads", "myfile.txt").writeText("file 1 content")
    return mapOf(
        // Files and folders of the folder uploads : they go out as one zip file.
        "__am__downloadFileOrFolderPaths" to listOf("myfile.txt", "someFolder"),
        "__am__downloadFolderFileName" to "zipFile.zip",   // the name of the zip file
    )
}

Download file from Angular sent by custom API

async function downloadFileFromBrowser() {
    const requestPayload = {};
    const requestHeaders = {};
    const resp = <any>await this.http.post(`API Maker custom API endpoint`, requestPayload, {
        headers: requestHeaders,
        responseType: <any>'text',
        observe: 'response',
    }).toPromise();
    const fileName = resp.headers.get('content-filename');
    await this.downloadFileFromBlob('application/zip', resp.body, fileName);
    return resp;
}

async function downloadFileFromBlob(contentType, base64Data: any, fileName) {
    const a = document.createElement('a');
    document.body.appendChild(a);
    a.style.display = 'none';

    let blob;
    if (base64Data instanceof Blob) {
        blob = base64Data;
    } else {
        const base64Response = await fetch(`data:${contentType};base64,` + base64Data);
        blob = await base64Response.blob();
    }

    const url = window.URL.createObjectURL(blob);
    a.href = url;
    a.download = fileName;
    a.click();
    window.URL.revokeObjectURL(url);
}

Multi-tenant

  • Call a custom API with the x-am-tenant-username header to run it for one tenant of a multi-tenant instance.
  • 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, and each instance finds the tenant in its own tenants table.
  • instance: 'crm::globex' names another tenant for one call, and an empty tenant header names the structure database. See Custom APIs of a multi-tenant instance.
GET /api/custom-api/admin/customers-report
x-am-tenant-username: acme
1
2
3
4
5
6
7
8
9
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    const tenant = g.req.params.tenantUsername; // 'acme'
    const customers = await g.sys.db.getAll({ instance: 'crm', database: 'crm', collection: 'customers' }); // from the database of acme
    const customersOfGlobex = await g.sys.db.count({ instance: 'crm::globex', database: 'crm', collection: 'customers' });
    return { tenant, customers, customersOfGlobex };
}
module.exports = main;
1
2
3
4
5
6
7
8
from am import G


async def main(g: G):
    tenant = g.req.params.tenantUsername  # 'acme'
    customers = await g.sys.db.get_all('crm', 'crm', 'customers')  # from the database of acme
    customers_of_globex = await g.sys.db.count('crm::globex', 'crm', 'customers')
    return {'tenant': tenant, 'customers': customers, 'customersOfGlobex': customers_of_globex}
1
2
3
4
5
6
7
8
<?php
function main(AmG $g)
{
    $tenant = $g->req->params->tenantUsername; // 'acme'
    $customers = $g->sys->db->getAll('crm', 'crm', 'customers'); // from the database of acme
    $customersOfGlobex = $g->sys->db->count('crm::globex', 'crm', 'customers');
    return ['tenant' => $tenant, 'customers' => $customers, 'customersOfGlobex' => $customersOfGlobex];
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var tenant = g.Req.Params.GetString("tenantUsername"); // "acme"
        var customers = await g.Sys.Db.GetAll(new GetAll("crm", "crm", "customers")); // from the database of acme
        var customersOfGlobex = await g.Sys.Db.Count(new Count("crm::globex", "crm", "customers"));
        return new { tenant, customers, customersOfGlobex };
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    tenant := g.Req.Params().String("tenantUsername") // "acme"
    customers, err := g.Sys.Db.GetAll(am.GetAll{Instance: "crm", Database: "crm", Collection: "customers"}) // from the database of acme
    if err != nil {
        return nil, err
    }
    customersOfGlobex, err := g.Sys.Db.Count(am.Count{Instance: "crm::globex", Database: "crm", Collection: "customers"})
    if err != nil {
        return nil, err
    }
    return am.M{"tenant": tenant, "customers": customers, "customersOfGlobex": customersOfGlobex}, nil
}
1
2
3
4
5
6
7
8
use am::{Count, Errors, G, GetAll, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let tenant = g.req.params()["tenantUsername"].clone(); // "acme"
    let customers = g.sys.db.get_all(GetAll::new("crm", "crm", "customers"))?; // from the database of acme
    let customers_of_globex = g.sys.db.count(Count::new("crm::globex", "crm", "customers"))?;
    Ok(json!({ "tenant": tenant, "customers": customers, "customersOfGlobex": customers_of_globex }))
}
import am.Count;
import am.G;
import am.GetAll;
import am.JsonObject;
import java.util.List;

class Api {
    static Object main(G g) {
        String tenant = g.req.getParams().getString("tenantUsername"); // "acme"
        List<JsonObject> customers = g.sys.db.getAll(new GetAll("crm", "crm", "customers")); // from the database of acme
        long customersOfGlobex = g.sys.db.count(new Count("crm::globex", "crm", "customers"));
        return JsonObject.of("tenant", tenant, "customers", customers, "customersOfGlobex", customersOfGlobex);
    }
}
import am.Count
import am.G
import am.GetAll

fun main(g: G): Any? {
    val tenant = g.req.params.getString("tenantUsername") // "acme"
    val customers = g.sys.db.getAll(GetAll("crm", "crm", "customers")) // from the database of acme
    val customersOfGlobex = g.sys.db.count(Count("crm::globex", "crm", "customers"))
    return mapOf("tenant" to tenant, "customers" to customers, "customersOfGlobex" to customersOfGlobex)
}

The samples of a new account

A new account comes with custom APIs under /default/… which show the features at work. Each one is written below in every language : the settings (always TypeScript) and the code of the language of your tab.

Sample Path Shows
Login /default/login the token of the app and of the person, one message for every failure
Data Isolation /default/data-isolation only the rows of the person who calls
Deep Populate Orders, Deep Populate Order Items /default/deep-populate-orders, /default/deep-populate-order-items rows with the rows they point to, on one and two levels
Find Join Orders, Find Join Products /default/find-join-orders, /default/find-join-products a filter on a column of another table
Caching Example /default/caching-example an answer kept in the cache until its tables change
Get Captcha, Login With Captcha /default/get-captcha, /default/login-with-captcha a value kept for a while in the cache, used once
WebSocket Notify /default/ws-notify a message to one person over WebSocket
Collection Copy Stream, Database Copy Stream /default/collection-copy, /default/database-copy rows read one by one : the memory stays the same whatever the size of the table
S3 Upload, S3 Download, S3 List, Image Upload /default/s3-upload… files in S3, with a package of the language

Most of them are NO_ACCESS in the account : open one with apiAccessType in its settings before an application calls it.

Sign in : the token of the app and of the person

The login of an application : it checks the email and the password with the token provider default_db_login, and answers the two tokens every later call sends, x-am-authorization (the app, an API user) and x-am-user-authorization (the person).

Its settings
import * as T from 'types';

const customApi: T.ICustomApiSettingsTypes = {
    name: 'Login',
    path: '/default/login',
    requestMethod: T.ERequestMethod.POST,
    apiAccessType: T.EAPIAccessType.IS_PUBLIC,   // the caller has no token yet
    errorList: ['Invalid email or password'],    // one message for every failure : it never tells which emails exist
    reqBodySchema: {                             // checked before the code runs
        email: { __type: T.EType.string, validations: { required: true } },
        password: { __type: T.EType.string, validations: { required: true } },
    },
};
module.exports = customApi;
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    const email = String(g.req.body.email || '').trim().toLowerCase();   // as the schema of customers stores it
    const password = String(g.req.body.password || '');
    const refused = [{ code: 401, message: 'Invalid email or password' }];
    if (!email || !password) throw refused;

    // The person, without its password : the token provider checks the password, not this read.
    const customers = await g.sys.db.query({
        instance: 'mongodb', database: 'shop', collection: 'customers',
        find: { email }, select: '-password', limit: 1,
    });
    if (!customers.length) throw refused;

    // The token of the app : the password of the API user is read from the secret, never written in the code.
    const appPassword = await g.sys.system.getSecret('common.apiUserPasswords.default');
    const app = await g.sys.system.getToken({ u: 'default', p: appPassword });
    // The token of the person : default_db_login compares the password with the stored one.
    let person;
    try {
        person = await g.sys.system.getToken({ name: 'default_db_login', u: email, p: password });
    } catch (e) {
        throw refused;
    }
    return { amToken: app.token, userToken: person.token, refreshToken: person.refresh_token, customer: customers[0] };
}
module.exports = main;
from am import Errors, G


async def main(g: G):
    email = str(g.req.body.email or '').strip().lower()   # as the schema of customers stores it
    password = str(g.req.body.password or '')
    if not email or not password:
        raise Errors('Invalid email or password', code=401)

    # The person, without its password : the token provider checks the password, not this read.
    customers = await g.sys.db.query('mongodb', 'shop', 'customers', find={'email': email}, select='-password', limit=1)
    if not customers:
        raise Errors('Invalid email or password', code=401)

    # The token of the app : the password of the API user is read from the secret, never written in the code.
    app_password = await g.sys.system.get_secret('common.apiUserPasswords.default')
    app = await g.sys.system.get_token('default', app_password)
    # The token of the person : default_db_login compares the password with the stored one.
    try:
        person = await g.sys.system.get_token(email, password, name='default_db_login')
    except Errors:
        raise Errors('Invalid email or password', code=401)
    return {'amToken': app.token, 'userToken': person.token, 'refreshToken': person.refresh_token, 'customer': customers[0]}
<?php
function main(AmG $g)
{
    $email = strtolower(trim((string) ($g->req->body->email ?? '')));   // as the schema of customers stores it
    $password = (string) ($g->req->body->password ?? '');
    if ($email === '' || $password === '') {
        throw new AmErrors('Invalid email or password', 401);
    }

    // The person, without its password : the token provider checks the password, not this read.
    $customers = $g->sys->db->query('mongodb', 'shop', 'customers', find: ['email' => $email], select: '-password', limit: 1);
    if (!$customers) {
        throw new AmErrors('Invalid email or password', 401);
    }

    // The token of the app : the password of the API user is read from the secret, never written in the code.
    $appPassword = $g->sys->system->getSecret('common.apiUserPasswords.default');
    $app = $g->sys->system->getToken('default', $appPassword);
    // The token of the person : default_db_login compares the password with the stored one.
    try {
        $person = $g->sys->system->getToken($email, $password, name: 'default_db_login');
    } catch (AmErrors $refused) {
        throw new AmErrors('Invalid email or password', 401);
    }
    return ['amToken' => $app->token, 'userToken' => $person->token, 'refreshToken' => $person->refresh_token, 'customer' => $customers[0]];
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var email = g.Req.Body.GetString("email", "").Trim().ToLowerInvariant();   // as the schema of customers stores it
        var password = g.Req.Body.GetString("password", "");
        if (email == "" || password == "") throw new AmErrors("Invalid email or password", 401);

        // The person, without its password : the token provider checks the password, not this read.
        var customers = await g.Sys.Db.Query(new Query("mongodb", "shop", "customers")
        {
            Find = new JsonObject { ["email"] = email }, Select = "-password", Limit = 1,
        });
        if (customers.Count == 0) throw new AmErrors("Invalid email or password", 401);

        // The token of the app : the password of the API user is read from the secret, never written in the code.
        var appPassword = (await g.Sys.System.GetSecret("common.apiUserPasswords.default"))?.GetValue<string>();
        var app = await g.Sys.System.GetToken(new GetToken("default", appPassword));
        // The token of the person : default_db_login compares the password with the stored one.
        try
        {
            var person = await g.Sys.System.GetToken(new GetToken(email, password) { Name = "default_db_login" });
            return new { amToken = app.Token, userToken = person.Token, refreshToken = person.RefreshToken, customer = customers[0] };
        }
        catch (AmErrors)
        {
            throw new AmErrors("Invalid email or password", 401);
        }
    }
}
package code

import (
    "strings"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    body := g.Req.Body()
    email := strings.ToLower(strings.TrimSpace(body.String("email"))) // as the schema of customers stores it
    password := body.String("password")
    if email == "" || password == "" {
        return nil, am.Fail(401, "Invalid email or password")
    }

    // The person, without its password : the token provider checks the password, not this read.
    customers, err := g.Sys.Db.Query(am.Query{
        Instance: "mongodb", Database: "shop", Collection: "customers",
        Find: am.M{"email": email}, Select: "-password", Limit: 1,
    })
    if err != nil {
        return nil, err
    }
    if len(customers) == 0 {
        return nil, am.Fail(401, "Invalid email or password")
    }

    // The token of the app : the password of the API user is read from the secret, never written in the code.
    secret, err := g.Sys.System.GetSecret("common.apiUserPasswords.default")
    if err != nil {
        return nil, err
    }
    appPassword, _ := secret.(string)
    app, err := g.Sys.System.GetToken(am.GetToken{U: "default", P: appPassword})
    if err != nil {
        return nil, err
    }
    // The token of the person : default_db_login compares the password with the stored one.
    person, err := g.Sys.System.GetToken(am.GetToken{Name: "default_db_login", U: email, P: password})
    if err != nil {
        return nil, am.Fail(401, "Invalid email or password")
    }
    return am.M{"amToken": app.Token, "userToken": person.Token, "refreshToken": person.RefreshToken, "customer": customers[0]}, nil
}
use am::{Errors, G, GetToken, Query, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let body = g.req.body();
    let email = body["email"].as_str().unwrap_or_default().trim().to_lowercase(); // as the schema of customers stores it
    let password = body["password"].as_str().unwrap_or_default().to_owned();
    if email.is_empty() || password.is_empty() {
        return Err(am::fail(401, "Invalid email or password"));
    }

    // The person, without its password : the token provider checks the password, not this read.
    let customers = g.sys.db.query(
        Query::new("mongodb", "shop", "customers").find(json!({ "email": email })).select("-password").limit(1),
    )?;
    let Some(customer) = customers.first().cloned() else {
        return Err(am::fail(401, "Invalid email or password"));
    };

    // The token of the app : the password of the API user is read from the secret, never written in the code.
    let app_password = g.sys.system.get_secret("common.apiUserPasswords.default", None)?;
    let app = g.sys.system.get_token(GetToken::new("default", app_password.as_str().unwrap_or_default()))?;
    // The token of the person : default_db_login compares the password with the stored one.
    let person = g
        .sys
        .system
        .get_token(GetToken::new(&email, &password).name("default_db_login"))
        .map_err(|_| am::fail(401, "Invalid email or password"))?;
    Ok(json!({ "amToken": app.token, "userToken": person.token, "refreshToken": person.refresh_token, "customer": customer }))
}
import am.AmErrors;
import am.G;
import am.GetToken;
import am.JsonObject;
import am.Query;
import am.TokenResponse;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        JsonObject body = g.req.getBody();
        String email = body.getString("email", "").trim().toLowerCase();   // as the schema of customers stores it
        String password = body.getString("password", "");
        if (email.isEmpty() || password.isEmpty()) throw new AmErrors("Invalid email or password", 401);

        // The person, without its password : the token provider checks the password, not this read.
        List<JsonObject> customers = g.sys.db.query(new Query("mongodb", "shop", "customers")
            .find(Map.of("email", email)).select("-password").limit(1));
        if (customers.isEmpty()) throw new AmErrors("Invalid email or password", 401);

        // The token of the app : the password of the API user is read from the secret, never written in the code.
        String appPassword = (String) g.sys.system.getSecret("common.apiUserPasswords.default");
        TokenResponse app = g.sys.system.getToken(new GetToken("default", appPassword));
        // The token of the person : default_db_login compares the password with the stored one.
        TokenResponse person;
        try {
            person = g.sys.system.getToken(new GetToken(email, password).name("default_db_login"));
        } catch (AmErrors refused) {
            throw new AmErrors("Invalid email or password", 401);
        }
        return Map.of("amToken", app.getToken(), "userToken", person.getToken(), "refreshToken", person.getRefreshToken(), "customer", customers.get(0));
    }
}
import am.AmErrors
import am.G
import am.GetToken
import am.Query

fun main(g: G): Any? {
    val email = g.req.body.getString("email", "").trim().lowercase()   // as the schema of customers stores it
    val password = g.req.body.getString("password", "")
    if (email.isEmpty() || password.isEmpty()) throw AmErrors("Invalid email or password", 401)

    // The person, without its password : the token provider checks the password, not this read.
    val customers = g.sys.db.query(Query("mongodb", "shop", "customers").find(mapOf("email" to email)).select("-password").limit(1))
    if (customers.isEmpty()) throw AmErrors("Invalid email or password", 401)

    // The token of the app : the password of the API user is read from the secret, never written in the code.
    val appPassword = g.sys.system.getSecret("common.apiUserPasswords.default") as String
    val app = g.sys.system.getToken(GetToken("default", appPassword))
    // The token of the person : default_db_login compares the password with the stored one.
    val person = try {
        g.sys.system.getToken(GetToken(email, password).name("default_db_login"))
    } catch (refused: AmErrors) {
        throw AmErrors("Invalid email or password", 401)
    }
    return mapOf("amToken" to app.token, "userToken" to person.token, "refreshToken" to person.refreshToken, "customer" to customers[0])
}
  • The API user default and its password path common.apiUserPasswords.default are those of a new account. Use the API user of the application, with only the groups it needs.
  • The two tokens in one call : Who calls.

Only the rows of the person who calls

The person comes from its token (g.req.auth.authAMDB, the row the token provider read), never from the request : nobody can ask for the orders of somebody else.

import * as T from 'types';

async function main(g: T.IAMGlobal) {
    const me = g.req.auth.authAMDB?.customer_id;   // its row of customers, as its token says
    if (me === undefined || me === null) throw [{ code: 401, message: 'Please sign in.' }];
    return await g.sys.db.getAll({
        instance: 'mongodb', database: 'shop', collection: 'orders',
        queryParams: { find: { customer_id: me }, sort: '-created_at', limit: 50 },
    });
}
module.exports = main;
1
2
3
4
5
6
7
8
from am import Errors, G


async def main(g: G):
    person = g.req.auth.authAMDB   # its row of customers, as its token says
    if not person or person.customer_id is None:
        raise Errors('Please sign in.', code=401)
    return await g.sys.db.get_all('mongodb', 'shop', 'orders', find={'customer_id': person.customer_id}, sort='-created_at', limit=50)
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $me = $g->req->auth->authAMDB->customer_id ?? null;   // its row of customers, as its token says
    if ($me === null) {
        throw new AmErrors('Please sign in.', 401);
    }
    return $g->sys->db->getAll('mongodb', 'shop', 'orders', find: ['customer_id' => $me], sort: '-created_at', limit: 50);
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var me = g.Req.Auth.GetObject("authAMDB")["customer_id"];   // its row of customers, as its token says
        if (me is null) throw new AmErrors("Please sign in.", 401);
        return await g.Sys.Db.GetAll(new GetAll("mongodb", "shop", "orders")
        {
            Find = new JsonObject { ["customer_id"] = me.DeepClone() },
            Sort = "-created_at", Limit = 50,
        });
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    me := g.Req.Auth().Map("authAMDB").Get("customer_id") // its row of customers, as its token says
    if me == nil {
        return nil, am.Fail(401, "Please sign in.")
    }
    return g.Sys.Db.GetAll(am.GetAll{
        Instance: "mongodb", Database: "shop", Collection: "orders",
        Find: am.M{"customer_id": me}, Sort: "-created_at", Limit: 50,
    })
}
use am::{Errors, G, GetAll, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let me = g.req.auth()["authAMDB"]["customer_id"].clone(); // its row of customers, as its token says
    if me.is_null() {
        return Err(am::fail(401, "Please sign in."));
    }
    let orders = g.sys.db.get_all(GetAll::new("mongodb", "shop", "orders").find(json!({ "customer_id": me })).sort("-created_at").limit(50))?;
    Ok(json!(orders))
}
import am.AmErrors;
import am.G;
import am.GetAll;
import java.util.Map;

class Api {
    static Object main(G g) {
        Object me = g.req.getAuth().getObject("authAMDB").get("customer_id");   // its row of customers, as its token says
        if (me == null) throw new AmErrors("Please sign in.", 401);
        return g.sys.db.getAll(new GetAll("mongodb", "shop", "orders").find(Map.of("customer_id", me)).sort("-created_at").limit(50));
    }
}
1
2
3
4
5
6
7
8
import am.AmErrors
import am.G
import am.GetAll

fun main(g: G): Any? {
    val me = g.req.auth.getObject("authAMDB")["customer_id"] ?: throw AmErrors("Please sign in.", 401)   // its row of customers, as its token says
    return g.sys.db.getAll(GetAll("mongodb", "shop", "orders").find(mapOf("customer_id" to me)).sort("-created_at").limit(50))
}
  • The APIs of the tables need the same rule : a row scoping pre hook puts the person into the filter of every API of the table. The samples restrictToOwnerInstance, restrictToOwnerDatabase and restrictToOwnerOrders of a new account are such hooks, inactive.

Rows with the rows they point to : deep

deep replaces the id a column holds with the row it names : each order comes with its customer. See deep.

Orders with their customer, and the total count
1
2
3
4
5
6
7
8
9
return await g.sys.db.getAll({
    instance: 'mongodb', database: 'shop', collection: 'orders',
    queryParams: {
        find: { status: 'PAID' },
        select: 'order_no,total_cents,status,customer_id',
        sort: '-created_at', limit: 50, getTotalCount: true,
        deep: [{ s_key: 'customer_id', t_db: 'shop', t_col: 'customers', t_key: 'customer_id', select: 'customer_id,email,country' }],
    },
}, true);   // true : the whole answer, with totalCount
Orders with their customer, and the total count
1
2
3
4
5
6
7
return await g.sys.db.full.get_all(   # full : the whole answer, with totalCount
    'mongodb', 'shop', 'orders',
    find={'status': 'PAID'},
    select='order_no,total_cents,status,customer_id',
    sort='-created_at', limit=50, get_total_count=True,
    deep=[DeepItem('customer_id', t_db='shop', t_col='customers', t_key='customer_id', select='customer_id,email,country')],
)
Orders with their customer, and the total count
1
2
3
4
5
6
7
return $g->sys->db->full->getAll(   // full : the whole answer, with totalCount
    'mongodb', 'shop', 'orders',
    find: ['status' => 'PAID'],
    select: 'order_no,total_cents,status,customer_id',
    sort: '-created_at', limit: 50, getTotalCount: true,
    deep: [new DeepItem('customer_id', tDb: 'shop', tCol: 'customers', tKey: 'customer_id', select: 'customer_id,email,country')],
);
Orders with their customer, and the total count
1
2
3
4
5
6
7
return await g.Sys.Db.Full.GetAll(new GetAll("mongodb", "shop", "orders")   // Full : the whole answer, with totalCount
{
    Find = new JsonObject { ["status"] = "PAID" },
    Select = "order_no,total_cents,status,customer_id",
    Sort = "-created_at", Limit = 50, GetTotalCount = true,
    Deep = new[] { new DeepItem("customer_id") { TDb = "shop", TCol = "customers", TKey = "customer_id", Select = "customer_id,email,country" } },
});
Orders with their customer, and the total count
1
2
3
4
5
6
7
return g.Sys.Db.Full.GetAll(am.GetAll{ // Full : the whole answer, with totalCount
    Instance: "mongodb", Database: "shop", Collection: "orders",
    Find:   am.M{"status": "PAID"},
    Select: "order_no,total_cents,status,customer_id",
    Sort:   "-created_at", Limit: 50, GetTotalCount: true,
    Deep:   []am.DeepItem{am.DeepItem{SKey: "customer_id", TDb: "shop", TCol: "customers", TKey: "customer_id", Select: "customer_id,email,country"}},
})
Orders with their customer, and the total count
1
2
3
4
5
6
7
8
let orders = g.sys.db.full.get_all( // full : the whole answer, with totalCount
    GetAll::new("mongodb", "shop", "orders")
        .find(json!({ "status": "PAID" }))
        .select("order_no,total_cents,status,customer_id")
        .sort("-created_at").limit(50).get_total_count(true)
        .deep(vec![DeepItem::new("customer_id").t_db("shop").t_col("customers").t_key("customer_id").select("customer_id,email,country")]),
)?;
return Ok(json!({ "success": orders.success, "data": orders.data, "totalCount": orders.total_count }));
Orders with their customer, and the total count
1
2
3
4
5
6
var orders = g.sys.db.full.getAll(new GetAll("mongodb", "shop", "orders")   // full : the whole answer, with totalCount
    .find(Map.of("status", "PAID"))
    .select("order_no,total_cents,status,customer_id")
    .sort("-created_at").limit(50).getTotalCount(true)
    .deep(List.of(new DeepItem("customer_id").tDb("shop").tCol("customers").tKey("customer_id").select("customer_id,email,country"))));
return orders;
Orders with their customer, and the total count
1
2
3
4
5
6
7
return g.sys.db.full.getAll(   // full : the whole answer, with totalCount
    GetAll("mongodb", "shop", "orders")
        .find(mapOf("status" to "PAID"))
        .select("order_no,total_cents,status,customer_id")
        .sort("-created_at").limit(50).getTotalCount(true)
        .deep(listOf(DeepItem("customer_id").tDb("shop").tCol("customers").tKey("customer_id").select("customer_id,email,country"))),
)

A deep inside a deep goes one level further : each order item comes with its product, and each product with its category.

Two levels
return await g.sys.db.getAll({
    instance: 'mongodb', database: 'shop', collection: 'orderItems',
    queryParams: {
        select: 'order_id,product_id,qty,unit_price_cents',
        limit: 200,
        deep: [{
            s_key: 'product_id', t_db: 'shop', t_col: 'products', t_key: '_id', select: 'name,slug,price_cents,category',
            deep: [{ s_key: 'category', t_db: 'shop', t_col: 'categories', t_key: '_id', select: 'name,slug' }],
        }],
    },
});
Two levels
1
2
3
4
5
6
7
8
9
return await g.sys.db.get_all(
    'mongodb', 'shop', 'orderItems',
    select='order_id,product_id,qty,unit_price_cents',
    limit=200,
    deep=[DeepItem(
        'product_id', t_db='shop', t_col='products', t_key='_id', select='name,slug,price_cents,category',
        deep=[DeepItem('category', t_db='shop', t_col='categories', t_key='_id', select='name,slug')],
    )],
)
Two levels
1
2
3
4
5
6
7
8
9
return $g->sys->db->getAll(
    'mongodb', 'shop', 'orderItems',
    select: 'order_id,product_id,qty,unit_price_cents',
    limit: 200,
    deep: [new DeepItem(
        'product_id', tDb: 'shop', tCol: 'products', tKey: '_id', select: 'name,slug,price_cents,category',
        deep: [new DeepItem('category', tDb: 'shop', tCol: 'categories', tKey: '_id', select: 'name,slug')],
    )],
);
Two levels
return await g.Sys.Db.GetAll(new GetAll("mongodb", "shop", "orderItems")
{
    Select = "order_id,product_id,qty,unit_price_cents",
    Limit = 200,
    Deep = new[]
    {
        new DeepItem("product_id")
        {
            TDb = "shop", TCol = "products", TKey = "_id", Select = "name,slug,price_cents,category",
            Deep = new[] { new DeepItem("category") { TDb = "shop", TCol = "categories", TKey = "_id", Select = "name,slug" } },
        },
    },
});
Two levels
1
2
3
4
5
6
7
8
9
return g.Sys.Db.GetAll(am.GetAll{
    Instance: "mongodb", Database: "shop", Collection: "orderItems",
    Select: "order_id,product_id,qty,unit_price_cents",
    Limit:  200,
    Deep: []am.DeepItem{am.DeepItem{
        SKey: "product_id", TDb: "shop", TCol: "products", TKey: "_id", Select: "name,slug,price_cents,category",
        Deep: []am.DeepItem{am.DeepItem{SKey: "category", TDb: "shop", TCol: "categories", TKey: "_id", Select: "name,slug"}},
    }},
})
Two levels
1
2
3
4
5
6
7
8
let items = g.sys.db.get_all(
    GetAll::new("mongodb", "shop", "orderItems")
        .select("order_id,product_id,qty,unit_price_cents")
        .limit(200)
        .deep(vec![DeepItem::new("product_id").t_db("shop").t_col("products").t_key("_id").select("name,slug,price_cents,category")
            .deep(vec![DeepItem::new("category").t_db("shop").t_col("categories").t_key("_id").select("name,slug")])]),
)?;
return Ok(json!(items));
Two levels
1
2
3
4
5
6
var items = g.sys.db.getAll(new GetAll("mongodb", "shop", "orderItems")
    .select("order_id,product_id,qty,unit_price_cents")
    .limit(200)
    .deep(List.of(new DeepItem("product_id").tDb("shop").tCol("products").tKey("_id").select("name,slug,price_cents,category")
        .deep(List.of(new DeepItem("category").tDb("shop").tCol("categories").tKey("_id").select("name,slug"))))));
return items;
Two levels
1
2
3
4
5
6
7
return g.sys.db.getAll(
    GetAll("mongodb", "shop", "orderItems")
        .select("order_id,product_id,qty,unit_price_cents")
        .limit(200)
        .deep(listOf(DeepItem("product_id").tDb("shop").tCol("products").tKey("_id").select("name,slug,price_cents,category")
            .deep(listOf(DeepItem("category").tDb("shop").tCol("categories").tKey("_id").select("name,slug"))))),
)
  • t_col, t_key and t_db can be left out when the schema of the column says where it points (database, collection, column of the property).

A filter on a column of another table : find join

A column which points to another table (a relation of the schema) is followed in find with a dot : 'customer_id.country' is the country of the customer of the order. It narrows the rows ; deep adds the related row. See find.

The paid orders of the customers of India
1
2
3
4
5
6
return await g.sys.db.query({
    instance: 'mongodb', database: 'shop', collection: 'orders',
    find: { status: 'PAID', 'customer_id.country': 'IN' },   // a column of orders, then one of customers
    select: 'order_no,total_cents,status,customer_id',
    limit: 50,
});
The paid orders of the customers of India
1
2
3
4
5
6
return await g.sys.db.query(
    'mongodb', 'shop', 'orders',
    find={'status': 'PAID', 'customer_id.country': 'IN'},   # a column of orders, then one of customers
    select='order_no,total_cents,status,customer_id',
    limit=50,
)
The paid orders of the customers of India
1
2
3
4
5
6
return $g->sys->db->query(
    'mongodb', 'shop', 'orders',
    find: ['status' => 'PAID', 'customer_id.country' => 'IN'],   // a column of orders, then one of customers
    select: 'order_no,total_cents,status,customer_id',
    limit: 50,
);
The paid orders of the customers of India
1
2
3
4
5
6
return await g.Sys.Db.Query(new Query("mongodb", "shop", "orders")
{
    Find = new JsonObject { ["status"] = "PAID", ["customer_id.country"] = "IN" },   // a column of orders, then one of customers
    Select = "order_no,total_cents,status,customer_id",
    Limit = 50,
});
The paid orders of the customers of India
1
2
3
4
5
6
return g.Sys.Db.Query(am.Query{
    Instance: "mongodb", Database: "shop", Collection: "orders",
    Find:   am.M{"status": "PAID", "customer_id.country": "IN"}, // a column of orders, then one of customers
    Select: "order_no,total_cents,status,customer_id",
    Limit:  50,
})
The paid orders of the customers of India
1
2
3
4
5
6
7
let orders = g.sys.db.query(
    Query::new("mongodb", "shop", "orders")
        .find(json!({ "status": "PAID", "customer_id.country": "IN" })) // a column of orders, then one of customers
        .select("order_no,total_cents,status,customer_id")
        .limit(50),
)?;
return Ok(json!(orders));
The paid orders of the customers of India
1
2
3
4
5
var orders = g.sys.db.query(new Query("mongodb", "shop", "orders")
    .find(Map.of("status", "PAID", "customer_id.country", "IN"))   // a column of orders, then one of customers
    .select("order_no,total_cents,status,customer_id")
    .limit(50));
return orders;
The paid orders of the customers of India
1
2
3
4
5
6
return g.sys.db.query(
    Query("mongodb", "shop", "orders")
        .find(mapOf("status" to "PAID", "customer_id.country" to "IN"))   // a column of orders, then one of customers
        .select("order_no,total_cents,status,customer_id")
        .limit(50),
)

The same column can filter and populate : the active products of the category audio, each with its category.

Filter and populate
1
2
3
4
5
6
return await g.sys.db.query({
    instance: 'mongodb', database: 'shop', collection: 'products',
    find: { is_active: true, 'category.slug': 'audio' },
    deep: [{ s_key: 'category', t_db: 'shop', t_col: 'categories', t_key: '_id', select: 'name,slug' }],
    limit: 50,
});
Filter and populate
1
2
3
4
5
6
return await g.sys.db.query(
    'mongodb', 'shop', 'products',
    find={'is_active': True, 'category.slug': 'audio'},
    deep=[DeepItem('category', t_db='shop', t_col='categories', t_key='_id', select='name,slug')],
    limit=50,
)
Filter and populate
1
2
3
4
5
6
return $g->sys->db->query(
    'mongodb', 'shop', 'products',
    find: ['is_active' => true, 'category.slug' => 'audio'],
    deep: [new DeepItem('category', tDb: 'shop', tCol: 'categories', tKey: '_id', select: 'name,slug')],
    limit: 50,
);
Filter and populate
1
2
3
4
5
6
return await g.Sys.Db.Query(new Query("mongodb", "shop", "products")
{
    Find = new JsonObject { ["is_active"] = true, ["category.slug"] = "audio" },
    Deep = new[] { new DeepItem("category") { TDb = "shop", TCol = "categories", TKey = "_id", Select = "name,slug" } },
    Limit = 50,
});
Filter and populate
1
2
3
4
5
6
return g.Sys.Db.Query(am.Query{
    Instance: "mongodb", Database: "shop", Collection: "products",
    Find:  am.M{"is_active": true, "category.slug": "audio"},
    Deep:  []am.DeepItem{am.DeepItem{SKey: "category", TDb: "shop", TCol: "categories", TKey: "_id", Select: "name,slug"}},
    Limit: 50,
})
Filter and populate
1
2
3
4
5
6
7
let products = g.sys.db.query(
    Query::new("mongodb", "shop", "products")
        .find(json!({ "is_active": true, "category.slug": "audio" }))
        .deep(vec![DeepItem::new("category").t_db("shop").t_col("categories").t_key("_id").select("name,slug")])
        .limit(50),
)?;
return Ok(json!(products));
Filter and populate
1
2
3
4
5
var products = g.sys.db.query(new Query("mongodb", "shop", "products")
    .find(Map.of("is_active", true, "category.slug", "audio"))
    .deep(List.of(new DeepItem("category").tDb("shop").tCol("categories").tKey("_id").select("name,slug")))
    .limit(50));
return products;
Filter and populate
1
2
3
4
5
6
return g.sys.db.query(
    Query("mongodb", "shop", "products")
        .find(mapOf("is_active" to true, "category.slug" to "audio"))
        .deep(listOf(DeepItem("category").tDb("shop").tCol("categories").tKey("_id").select("name,slug")))
        .limit(50),
)
  • The path follows a relation forwards (a column which names database, collection and column in the schema). A virtual field is not read in find.

Cache the answer

The settings keep the answer in Redis until one of the tables it reads is written through API Maker : the code has nothing to do. See Automatic caching.

Its settings
import * as T from 'types';

const customApi: T.ICustomApiSettingsTypes = {
    name: 'Caching Example',
    path: '/default/caching-example',
    requestMethod: T.ERequestMethod.GET,
    apiAccessType: T.EAPIAccessType.TOKEN_ACCESS,
    errorList: [],
    enableCaching: true,                     // one answer per URL, body and headers which matter
    resetCacheOnModificationOf: [
        'DB: mongodb:shop:orders',           // forgotten when the orders change
        'DB: mongodb:shop:orderItems',       // or their items
    ],
};
module.exports = customApi;
Its code
1
2
3
4
5
const orders = await g.sys.db.getAll({
    instance: 'mongodb', database: 'shop', collection: 'orders',
    queryParams: { find: { status: 'PAID' }, select: 'order_no,total_cents', sort: '-created_at', limit: 20 },
});
return { orders };   // the next calls get this answer from the cache, until the orders change
Its code
1
2
3
4
5
orders = await g.sys.db.get_all(
    'mongodb', 'shop', 'orders',
    find={'status': 'PAID'}, select='order_no,total_cents', sort='-created_at', limit=20,
)
return {'orders': orders}   # the next calls get this answer from the cache, until the orders change
Its code
1
2
3
4
5
$orders = $g->sys->db->getAll(
    'mongodb', 'shop', 'orders',
    find: ['status' => 'PAID'], select: 'order_no,total_cents', sort: '-created_at', limit: 20,
);
return ['orders' => $orders];   // the next calls get this answer from the cache, until the orders change
Its code
1
2
3
4
5
var orders = await g.Sys.Db.GetAll(new GetAll("mongodb", "shop", "orders")
{
    Find = new JsonObject { ["status"] = "PAID" }, Select = "order_no,total_cents", Sort = "-created_at", Limit = 20,
});
return new { orders };   // the next calls get this answer from the cache, until the orders change
Its code
1
2
3
4
5
6
7
8
orders, err := g.Sys.Db.GetAll(am.GetAll{
    Instance: "mongodb", Database: "shop", Collection: "orders",
    Find: am.M{"status": "PAID"}, Select: "order_no,total_cents", Sort: "-created_at", Limit: 20,
})
if err != nil {
    return nil, err
}
return am.M{"orders": orders}, nil // the next calls get this answer from the cache, until the orders change
Its code
1
2
3
4
let orders = g.sys.db.get_all(
    GetAll::new("mongodb", "shop", "orders").find(json!({ "status": "PAID" })).select("order_no,total_cents").sort("-created_at").limit(20),
)?;
return Ok(json!({ "orders": orders })); // the next calls get this answer from the cache, until the orders change
Its code
1
2
3
var orders = g.sys.db.getAll(new GetAll("mongodb", "shop", "orders")
    .find(Map.of("status", "PAID")).select("order_no,total_cents").sort("-created_at").limit(20));
return Map.of("orders", orders);   // the next calls get this answer from the cache, until the orders change
Its code
1
2
3
4
val orders = g.sys.db.getAll(
    GetAll("mongodb", "shop", "orders").find(mapOf("status" to "PAID")).select("order_no,total_cents").sort("-created_at").limit(20),
)
return mapOf("orders" to orders)   // the next calls get this answer from the cache, until the orders change
  • An answer which must differ for every call (a captcha, a time) needs enableCaching: false.

A value kept for a while, used once : a captcha

The sample Get Captcha draws an image with the npm package svg-captcha (add it under Sandbox settings › packages) and keeps only its answer, in the cache, for three minutes :

Get Captcha, in TypeScript
import * as T from 'types';
const svgCaptcha = require('svg-captcha');

async function main(g: T.IAMGlobal) {
    const captcha = svgCaptcha.create({ size: 6, noise: 2 });
    // The key names the captcha, nothing in it gives the answer.
    const key = Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 12);
    await g.sys.cache.setKey('captcha:' + key, captcha.text, 180);   // only the answer, only on the server, 180 seconds
    return { key, svg: captcha.data, expiresInSeconds: 180 };
}
module.exports = main;

In another language, draw the image with a package of its registry and keep the answer the same way : g.sys.cache.set_key(key, text, 180) in Python, $g->sys->cache->setKey in PHP, g.Sys.Cache.SetKey in C# and Go, g.sys.cache.set_key(…, Some(180)) in Rust, g.sys.cache.setKey in Java and Kotlin. The login which gets the answer reads it, removes it at once (one try per image : a solved image can not be sent twice), and compares :

Check the captcha before the login
1
2
3
4
5
6
7
8
const key = 'captcha:' + String(g.req.body.captchaKey || '');
const expected = await g.sys.cache.getKey(key);
if (!expected) throw [{ code: 400, message: 'Captcha expired, please refresh it' }];
await g.sys.cache.removeKey(key);   // one try per image
if (String(expected).toLowerCase() !== String(g.req.body.captcha || '').trim().toLowerCase()) {
    throw [{ code: 400, message: 'Invalid captcha' }];
}
return { solved: true };   // then the login itself : see Sign in
Check the captcha before the login
1
2
3
4
5
6
7
8
key = 'captcha:' + str(g.req.body.captchaKey or '')
expected = await g.sys.cache.get_key(key)
if not expected:
    raise Errors('Captcha expired, please refresh it', code=400)
await g.sys.cache.remove_key(key)   # one try per image
if str(expected).lower() != str(g.req.body.captcha or '').strip().lower():
    raise Errors('Invalid captcha', code=400)
return {'solved': True}   # then the login itself : see Sign in
Check the captcha before the login
$key = 'captcha:' . (string) ($g->req->body->captchaKey ?? '');
$expected = $g->sys->cache->getKey($key);
if (!$expected) {
    throw new AmErrors('Captcha expired, please refresh it', 400);
}
$g->sys->cache->removeKey($key);   // one try per image
if (strtolower((string) $expected) !== strtolower(trim((string) ($g->req->body->captcha ?? '')))) {
    throw new AmErrors('Invalid captcha', 400);
}
return ['solved' => true];   // then the login itself : see Sign in
Check the captcha before the login
1
2
3
4
5
6
7
8
9
var key = "captcha:" + g.Req.Body.GetString("captchaKey", "");
var expected = await g.Sys.Cache.GetKey(key);
if (string.IsNullOrEmpty(expected)) throw new AmErrors("Captcha expired, please refresh it", 400);
await g.Sys.Cache.RemoveKey(key);   // one try per image
if (!string.Equals(expected, g.Req.Body.GetString("captcha", "").Trim(), StringComparison.OrdinalIgnoreCase))
{
    throw new AmErrors("Invalid captcha", 400);
}
return new { solved = true };   // then the login itself : see Sign in
Check the captcha before the login
import "strings"

key := "captcha:" + g.Req.Body().String("captchaKey")
expected, err := g.Sys.Cache.GetKey(key)
if err != nil {
    return nil, err
}
if expected == nil || *expected == "" {
    return nil, am.Fail(400, "Captcha expired, please refresh it")
}
if _, err := g.Sys.Cache.RemoveKey(key); err != nil { // one try per image
    return nil, err
}
if !strings.EqualFold(*expected, strings.TrimSpace(g.Req.Body().String("captcha"))) {
    return nil, am.Fail(400, "Invalid captcha")
}
return am.M{"solved": true}, nil // then the login itself : see Sign in
Check the captcha before the login
let key = format!("captcha:{}", g.req.body()["captchaKey"].as_str().unwrap_or_default());
let Some(expected) = g.sys.cache.get_key(&key)?.filter(|text| !text.is_empty()) else {
    return Err(am::fail(400, "Captcha expired, please refresh it"));
};
g.sys.cache.remove_key(&key)?; // one try per image
let answer = g.req.body()["captcha"].as_str().unwrap_or_default().trim().to_lowercase();
if expected.to_lowercase() != answer {
    return Err(am::fail(400, "Invalid captcha"));
}
Ok(json!({ "solved": true })) // then the login itself : see Sign in
Check the captcha before the login
1
2
3
4
5
6
7
8
String key = "captcha:" + g.req.getBody().getString("captchaKey", "");
String expected = g.sys.cache.getKey(key);
if (expected == null || expected.isEmpty()) throw new AmErrors("Captcha expired, please refresh it", 400);
g.sys.cache.removeKey(key);   // one try per image
if (!expected.equalsIgnoreCase(g.req.getBody().getString("captcha", "").trim())) {
    throw new AmErrors("Invalid captcha", 400);
}
return Map.of("solved", true);   // then the login itself : see Sign in
Check the captcha before the login
1
2
3
4
5
6
7
8
val key = "captcha:" + g.req.body.getString("captchaKey", "")
val expected = g.sys.cache.getKey(key)
if (expected.isNullOrEmpty()) throw AmErrors("Captcha expired, please refresh it", 400)
g.sys.cache.removeKey(key)   // one try per image
if (!expected.equals(g.req.body.getString("captcha", "").trim(), ignoreCase = true)) {
    throw AmErrors("Invalid captcha", 400)
}
return mapOf("solved" to true)   // then the login itself : see Sign in
  • Both APIs are IS_PUBLIC (the login screen calls them before anybody has a token) and enableCaching: false.
  • The messages of the captcha say what failed, so that the screen fetches a new image ; the message of the credentials stays one for every failure.

A message to one person over WebSocket

g.sys.system.emitEventWS sends the data to the clients subscribed to the WebSocket event whose criteria equal it : one message per person, no broadcast room. The event default/orders-updated is a WebSocket event of a new account.

Tell one customer
1
2
3
4
5
const email = String(g.req.body.email || '');
if (!email) throw [{ code: 400, message: 'Please provide email.' }];
// The clients which subscribed with the criteria { email: '<this email>' } get it.
await g.sys.system.emitEventWS('default/orders-updated', { email, type: 'order-updated', message: 'Your order was shipped.' });
return { emitted: true, email };
Tell one customer
1
2
3
4
5
6
email = str(g.req.body.email or '')
if not email:
    raise Errors('Please provide email.', code=400)
# The clients which subscribed with the criteria { email: '<this email>' } get it.
await g.sys.system.emit_event_ws('default/orders-updated', {'email': email, 'type': 'order-updated', 'message': 'Your order was shipped.'})
return {'emitted': True, 'email': email}
Tell one customer
1
2
3
4
5
6
7
$email = (string) ($g->req->body->email ?? '');
if ($email === '') {
    throw new AmErrors('Please provide email.', 400);
}
// The clients which subscribed with the criteria { email: '<this email>' } get it.
$g->sys->system->emitEventWS('default/orders-updated', ['email' => $email, 'type' => 'order-updated', 'message' => 'Your order was shipped.']);
return ['emitted' => true, 'email' => $email];
Tell one customer
1
2
3
4
5
var email = g.Req.Body.GetString("email", "");
if (email == "") throw new AmErrors("Please provide email.", 400);
// The clients which subscribed with the criteria { email: '<this email>' } get it.
await g.Sys.System.EmitEventWS("default/orders-updated", new JsonObject { ["email"] = email, ["type"] = "order-updated", ["message"] = "Your order was shipped." });
return new { emitted = true, email };
Tell one customer
1
2
3
4
5
6
7
8
9
email := g.Req.Body().String("email")
if email == "" {
    return nil, am.Fail(400, "Please provide email.")
}
// The clients which subscribed with the criteria { email: '<this email>' } get it.
if _, err := g.Sys.System.EmitEventWS("default/orders-updated", am.M{"email": email, "type": "order-updated", "message": "Your order was shipped."}); err != nil {
    return nil, err
}
return am.M{"emitted": true, "email": email}, nil
Tell one customer
1
2
3
4
5
6
7
let email = g.req.body()["email"].as_str().unwrap_or_default().to_owned();
if email.is_empty() {
    return Err(am::fail(400, "Please provide email."));
}
// The clients which subscribed with the criteria { email: '<this email>' } get it.
g.sys.system.emit_event_ws("default/orders-updated", json!({ "email": email, "type": "order-updated", "message": "Your order was shipped." }))?;
Ok(json!({ "emitted": true, "email": email }))
Tell one customer
1
2
3
4
5
String email = g.req.getBody().getString("email", "");
if (email.isEmpty()) throw new AmErrors("Please provide email.", 400);
// The clients which subscribed with the criteria { email: '<this email>' } get it.
g.sys.system.emitEventWS("default/orders-updated", Map.of("email", email, "type", "order-updated", "message", "Your order was shipped."));
return Map.of("emitted", true, "email", email);
Tell one customer
1
2
3
4
5
val email = g.req.body.getString("email", "")
if (email.isEmpty()) throw AmErrors("Please provide email.", 400)
// The clients which subscribed with the criteria { email: '<this email>' } get it.
g.sys.system.emitEventWS("default/orders-updated", mapOf("email" to email, "type" to "order-updated", "message" to "Your order was shipped."))
return mapOf("emitted" to true, "email" to email)

The page of the customer subscribes once it is connected :

In the browser
const ws = new WebSocket(`${wsUrl}?authTokenInfo=default_db_login&x-am-authorization=${appToken}&x-am-user-authorization=${userToken}`);
ws.onmessage = (event) => {
    const message = JSON.parse(event.data);
    if (message.type === 'CONNECTED' && message.response.connected) {
        ws.send(JSON.stringify({
            objType: 'REGISTER',
            onEvents: [{
                eventType: 'CUSTOM_WS_EVENTS', apiName: 'default/orders-updated', getEventData: true,
                condition: { conditionType: 'RESPONSE', criteria: { email: myEmail } },
            }],
        }));
    }
    if (message.type === 'NOTIFICATION') showMessage(message.response.eventData);
};

Rows read one by one : a table to a file

queryByStream hands the rows to a function one by one : the memory of the API stays the same whatever the size of the table. This API writes the paid orders into a CSV file, then sends it.

import * as T from 'types';
import * as fs from 'fs';
import * as path from 'path';

async function main(g: T.IAMGlobal) {
    await fs.promises.mkdir(path.join(__dirname, 'uploads'), { recursive: true });
    const file = fs.createWriteStream(path.join(__dirname, 'uploads', 'orders.csv'));
    const csv = (value: any) => '"' + String(value ?? '').replace(/"/g, '""') + '"';
    file.write('order_no,status,total_cents\n');
    await g.sys.db.queryByStream({
        instance: 'mongodb', database: 'shop', collection: 'orders',
        find: { status: 'PAID' }, select: 'order_no,status,total_cents',
    }, (order: any) => {
        file.write([order.order_no, order.status, order.total_cents].map(csv).join(',') + '\n');
    });
    await new Promise(done => file.end(done));
    return {
        __am__downloadFilePath: 'orders.csv',             // its name in the folder uploads
        __am__downloadFolderFileName: 'paid-orders.csv',  // the name the file is saved under
        __am__cleanupFileOrFolderPaths: 'orders.csv',     // removed once it is sent
    };
}
module.exports = main;
import csv
import os

from am import G, Query


async def main(g: G):
    os.makedirs('uploads', exist_ok=True)
    with open(os.path.join('uploads', 'orders.csv'), 'w', newline='', encoding='utf-8') as file:
        out = csv.writer(file)
        out.writerow(['order_no', 'status', 'total_cents'])

        def write(order):
            out.writerow([order.get('order_no'), order.get('status'), order.get('total_cents')])

        await g.sys.db.query_by_stream(
            Query('mongodb', 'shop', 'orders', find={'status': 'PAID'}, select='order_no,status,total_cents'),
            write,
        )
    return {
        '__am__downloadFilePath': 'orders.csv',             # its name in the folder uploads
        '__am__downloadFolderFileName': 'paid-orders.csv',  # the name the file is saved under
        '__am__cleanupFileOrFolderPaths': 'orders.csv',     # removed once it is sent
    }
<?php
function main(AmG $g)
{
    if (!is_dir('uploads')) {
        mkdir('uploads', 0777, true);
    }
    $file = fopen('uploads/orders.csv', 'w');
    fputcsv($file, ['order_no', 'status', 'total_cents']);
    $g->sys->db->queryByStream(
        new Query('mongodb', 'shop', 'orders', find: ['status' => 'PAID'], select: 'order_no,status,total_cents'),
        function ($order) use ($file) {
            fputcsv($file, [$order->order_no ?? '', $order->status ?? '', $order->total_cents ?? '']);
        },
    );
    fclose($file);
    return [
        '__am__downloadFilePath' => 'orders.csv',             // its name in the folder uploads
        '__am__downloadFolderFileName' => 'paid-orders.csv',  // the name the file is saved under
        '__am__cleanupFileOrFolderPaths' => 'orders.csv',     // removed once it is sent
    ];
}
using System.IO;

file static class Api
{
    public static async Task<object?> Main(G g)
    {
        Directory.CreateDirectory("uploads");
        static string Csv(JsonNode? value) => "\"" + (value?.ToString() ?? "").Replace("\"", "\"\"") + "\"";
        using (var file = new StreamWriter(Path.Combine("uploads", "orders.csv")))
        {
            file.WriteLine("order_no,status,total_cents");
            await g.Sys.Db.QueryByStream(new Query("mongodb", "shop", "orders")
            {
                Find = new JsonObject { ["status"] = "PAID" }, Select = "order_no,status,total_cents",
            }, order => file.WriteLine(string.Join(",", Csv(order["order_no"]), Csv(order["status"]), Csv(order["total_cents"]))));
        }
        return new JsonObject
        {
            ["__am__downloadFilePath"] = "orders.csv",             // its name in the folder uploads
            ["__am__downloadFolderFileName"] = "paid-orders.csv",  // the name the file is saved under
            ["__am__cleanupFileOrFolderPaths"] = "orders.csv",     // removed once it is sent
        };
    }
}
package code

import (
    "encoding/csv"
    "fmt"
    "os"
    "path/filepath"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    if err := os.MkdirAll("uploads", 0o755); err != nil {
        return nil, err
    }
    file, err := os.Create(filepath.Join("uploads", "orders.csv"))
    if err != nil {
        return nil, err
    }
    defer file.Close()
    out := csv.NewWriter(file)
    text := func(value any) string {
        if value == nil {
            return ""
        }
        return fmt.Sprint(value)
    }
    if err := out.Write([]string{"order_no", "status", "total_cents"}); err != nil {
        return nil, err
    }
    if err := g.Sys.Db.QueryByStream(am.Query{
        Instance: "mongodb", Database: "shop", Collection: "orders",
        Find: am.M{"status": "PAID"}, Select: "order_no,status,total_cents",
    }, func(order am.M) error {
        return out.Write([]string{text(order.Get("order_no")), text(order.Get("status")), text(order.Get("total_cents"))})
    }); err != nil {
        return nil, err
    }
    out.Flush()
    if err := out.Error(); err != nil {
        return nil, err
    }
    return am.M{
        "__am__downloadFilePath":         "orders.csv",      // its name in the folder uploads
        "__am__downloadFolderFileName":   "paid-orders.csv", // the name the file is saved under
        "__am__cleanupFileOrFolderPaths": "orders.csv",      // removed once it is sent
    }, nil
}
use std::io::Write;

use am::{Errors, G, Query, Value, json};

fn csv(value: &Value) -> String {
    let text = match value {
        Value::Null => String::new(),
        Value::String(text) => text.clone(),
        other => other.to_string(),
    };
    format!("\"{}\"", text.replace('"', "\"\""))
}

pub fn main(g: &mut G) -> Result<Value, Errors> {
    std::fs::create_dir_all("uploads")?;
    let mut file = std::io::BufWriter::new(std::fs::File::create("uploads/orders.csv")?);
    writeln!(file, "order_no,status,total_cents")?;
    g.sys.db.query_by_stream(
        Query::new("mongodb", "shop", "orders").find(json!({ "status": "PAID" })).select("order_no,status,total_cents"),
        |order| {
            writeln!(file, "{},{},{}", csv(&order["order_no"]), csv(&order["status"]), csv(&order["total_cents"]))?;
            Ok(())
        },
    )?;
    file.flush()?;
    Ok(json!({
        "__am__downloadFilePath": "orders.csv",            // its name in the folder uploads
        "__am__downloadFolderFileName": "paid-orders.csv", // the name the file is saved under
        "__am__cleanupFileOrFolderPaths": "orders.csv",    // removed once it is sent
    }))
}
import am.G;
import am.Query;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

class Api {
    static String csv(Object value) {
        return "\"" + (value == null ? "" : value.toString()).replace("\"", "\"\"") + "\"";
    }

    static Object main(G g) throws Exception {
        Files.createDirectories(Path.of("uploads"));
        try (PrintWriter file = new PrintWriter(Files.newBufferedWriter(Path.of("uploads", "orders.csv")))) {
            file.println("order_no,status,total_cents");
            g.sys.db.queryByStream(new Query("mongodb", "shop", "orders")
                .find(Map.of("status", "PAID")).select("order_no,status,total_cents"),
                order -> file.println(csv(order.get("order_no")) + "," + csv(order.get("status")) + "," + csv(order.get("total_cents"))));
        }
        return Map.of(
            "__am__downloadFilePath", "orders.csv",             // its name in the folder uploads
            "__am__downloadFolderFileName", "paid-orders.csv",  // the name the file is saved under
            "__am__cleanupFileOrFolderPaths", "orders.csv");    // removed once it is sent
    }
}
import am.G
import am.Query
import java.io.File

fun csv(value: Any?) = "\"" + (value?.toString() ?: "").replace("\"", "\"\"") + "\""

fun main(g: G): Any? {
    File("uploads").mkdirs()
    File("uploads", "orders.csv").printWriter().use { file ->
        file.println("order_no,status,total_cents")
        g.sys.db.queryByStream(
            Query("mongodb", "shop", "orders").find(mapOf("status" to "PAID")).select("order_no,status,total_cents"),
        ) { order -> file.println(csv(order["order_no"]) + "," + csv(order["status"]) + "," + csv(order["total_cents"])) }
    }
    return mapOf(
        "__am__downloadFilePath" to "orders.csv",             // its name in the folder uploads
        "__am__downloadFolderFileName" to "paid-orders.csv",  // the name the file is saved under
        "__am__cleanupFileOrFolderPaths" to "orders.csv",     // removed once it is sent
    )
}
  • The samples Collection Copy Stream and Database Copy Stream of a new account copy a table, or every table of a database, the same way : g.sys.db.gen.queryByStreamGen reads the rows without a schema, and the function saves them by batches of 1,000 with g.sys.db.gen.saveSingleOrMultipleGen, waiting for each batch before the next rows come.
  • getAllByStream does the same with the queryParams of a get all. See Query for get data by stream.

Files in S3

The samples S3 Upload, S3 Download, S3 List and Image Upload use the npm package @aws-sdk/client-s3 (and sharp to make the images smaller), with the keys of the bucket in the secret (s3.bucket, s3.region, s3.accessKeyId, s3.secretAccessKey, and s3.endpoint for MinIO, R2 or Spaces). The parts of API Maker are the same in every language : the keys from g.sys.system.getSecret('s3'), the uploaded file in g.req.body.files (see Files), and a download through the folder uploads :

S3 Download, in TypeScript
import * as T from 'types';
import * as fs from 'fs';
import * as path from 'path';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';

async function main(g: T.IAMGlobal): Promise<T.IDownloadResponse> {
    const s3 = await g.sys.system.getSecret('s3');
    if (!s3 || !s3.bucket) throw new Error('S3 secret is not configured.');
    const client = new S3Client({
        region: s3.region,
        endpoint: s3.endpoint || undefined,
        forcePathStyle: !!s3.endpoint,
        credentials: { accessKeyId: s3.accessKeyId, secretAccessKey: s3.secretAccessKey },
    });
    const key = String(g.req.query?.key || '');
    if (!key) throw new Error('Query param "key" is required.');

    // The object is written into the folder uploads, the one folder files are sent from.
    const obj = await client.send(new GetObjectCommand({ Bucket: s3.bucket, Key: key }));
    const name = path.basename(key);
    await new Promise<void>((resolve, reject) => {
        const out = fs.createWriteStream(path.join(__dirname, 'uploads', name));
        (obj.Body as NodeJS.ReadableStream).pipe(out);
        out.on('finish', () => resolve());
        out.on('error', reject);
    });
    return {
        __am__downloadFilePath: name,         // in the folder uploads
        __am__downloadFolderFileName: name,   // the name the file is saved under
        __am__cleanupFileOrFolderPaths: name, // removed once it is sent
    };
}
module.exports = main;
  • In another language : the SDK of AWS for it (boto3 for Python, aws/aws-sdk-php, AWSSDK.S3 for C#, github.com/aws/aws-sdk-go-v2/service/s3, aws-sdk-s3 for Rust, software.amazon.awssdk:s3 for Java and Kotlin), added to the packages of its language in Sandbox settings.