The 17 operations of a table All APIs
| Operation |
Method |
After …/<table> |
Body |
Id, after SCHEMA_ or GEN_ |
| Get all |
GET |
/ |
query params |
GET_ALL |
| Get all by stream |
GET |
/stream |
query params |
GET_ALL_STREAM |
| Get by id |
GET |
/get-by-id/:id[/:primaryKey] |
query params |
GET_BY_ID |
| Query for get data |
POST |
/query |
{ find, select, sort, skip, limit, deep, getTotalCount } |
POST_QUERY |
| Query by stream |
POST |
/query-stream |
the same as query |
POST_QUERY_STREAM |
| Count |
POST |
/count |
{ find } |
POST_COUNT |
| Distinct |
GET |
/distinct/:field[/:order] |
|
GET_DISTINCT |
| Distinct with query |
POST |
/distinct/:field[/:order] |
{ find } |
POST_DISTINCT_QUERY |
| Aggregate MongoDB |
POST |
/aggregate |
a pipeline array |
POST_AGGREGATE |
| Save single or multiple |
POST |
/save-single-or-multiple |
an object or an array |
POST_BULK_INSERT |
| Master save |
POST |
/master-save |
objects with their related objects |
MASTER_SAVE |
| Update by id |
PUT |
/update-by-id/:id[/:primaryKey] |
the fields to change |
PUT_UPDATE_BY_ID |
| Update many |
PUT |
/update-many |
{ find, updateData } |
UPDATE_MANY |
| Replace by id MongoDB |
PUT |
/replace-by-id/:id[/:primaryKey] |
the whole document |
PUT_REPLACE_BY_ID |
| Array operations MongoDB |
PUT |
/array-operations |
{ find, operations } |
ARRAY_OPERATIONS |
| Remove by id |
DELETE |
/:id[/:primaryKey] |
|
DEL_DELETE_BY_ID |
| Remove by query |
POST |
/query/delete |
{ find }, never empty |
POST_QUERY_DELETE |
The path goes after /api/schema/<user-path>/<instance>/<database>/<table>, or the same under /api/gen. MongoDB has all 17. MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB and Percona have 14. The schema APIs exist once a table has a schema. :primaryKey names another column to use as the key. The ids name the APIs in groups, hooks, events and WebSocket subscriptions : SCHEMA_GET_ALL.
| Who calls, how it runs |
Value |
x-am-authorization |
token of the API user (the app) |
x-am-user-authorization |
token of a person, from your users table |
x-google-authorization
x-azure-authorization
x-aws-authorization
x-custom-authorization |
token of a person, from that provider |
x-am-tenant-username |
tenant of a multi-tenant instance : acme |
x-am-sandbox-timeout |
milliseconds the code may run, 13000 by default |
x-am-run-in-sandbox |
0 any sandbox, 1 the first one, n one of the first n |
x-am-run-in-debugger |
true : the code stops at breakpoints, while debugging is on |
x-am-cache-control |
no_action reset_cache |
x-am-encrypted-payload |
true when the body is { dataEncFE } |
| The answer |
Value |
x-am-response-case |
noChange camelCase capitalCase constantCase dotCase headerCase noCase paramCase pascalCase pathCase sentenceCase snakeCase |
x-am-content-type-response |
application/json text/xml text/yaml text/plain text/html application/octet-stream |
x-am-response-object-type |
no_action make_flat : state_id_country_name |
x-am-get-encrypted-data |
no_encryption get_only_encryption get_data_and_encryption |
x-am-internationalization |
a language of i18N Management : Hindi |
x-am-meta |
true : time, groups and server in meta, in the admin panel only |
x-no-compression |
true : never compressed (else from 51200 bytes) |
accept-encoding |
gzip, else br, else deflate |
|
|
| Schema APIs |
/api/schema/<user-path>/<instance>/<db>/<table> |
| Generated APIs, no schema |
/api/gen/<user-path>/<instance>/<db>/<table> |
| Custom APIs |
/api/custom-api/<user-path>/<path> |
System APIs, all POST |
/api/system-api/<user-path>/<name> |
| A tenant |
<instance>::<tenant> in the path, or the header x-am-tenant-username |
| WebSocket |
wss://<api host>/?x-am-authorization=<token> |
| SSR website |
/ssr/<user-path>/<website>, or its own domain |
<user-path> is the API path of the account : admin for the first admin. <db> is the database.
POST /api/system-api/admin/token
{ "u": "default", "p": "…" } the app
{ "name": "users_tg", "u", "p" } a person
{ "refresh_token": "…" } renew
→ { token, refresh_token, expires_in }
|
|
x-am-authorization |
the app : an API user. Always. |
x-am-user-authorization |
a person of your users table |
x-google-authorization
x-azure-authorization
x-aws-authorization
x-custom-authorization |
a person signed in with Google, Azure AD, AWS Cognito or your own provider |
A token lives 259200 s (3 days), unless expiresInSeconds says otherwise. Its refresh token works 900 s longer.
| Access type |
Who may call |
TOKEN_ACCESS |
a token whose groups allow the API. The default. |
IS_PUBLIC |
anyone, without a token |
NO_ACCESS |
only your own code and API Testing |
Set it as apiAccessType in the settings of a table or a custom API. A call passes when a group of the API user grants the API. With a person token, a group of the person must grant it too.
|
|
| Security |
/token /encrypt-data /decrypt-data /hash-data /get-secret-by-name |
| Redis and cache |
/get-redis-key /set-redis-key /remove-redis-key /reset-redis-cache-db /reset-redis-cache-custom-apis /reset-redis-cache-system-apis |
| Databases |
/get-table-meta /create-indexes /drop-indexes /get-indexes /execute-plain-query /multi-tenant-instance-updated |
| Events |
/emit-event /emit-event-ws |
| Checks |
/is-valid-data-for-table /is-valid-data-for-custom-api /is-valid-connection-string |
| Outside |
/call-external-api |
All are POST …/system-api/<user-path>/…. From code they are g.sys.system.* and g.sys.cache.* : /token is getToken, /get-redis-key is cache.getKey. Their id is the path in capitals (ENCRYPT_DATA), but /token is GET_TOKEN and /get-secret-by-name is GET_SECRET.
|
|
find={status:'ACTIVE'} |
filter in JSON5, with MongoDB operators, on every database |
select=name,price · select=-password |
only these fields · all but these |
sort=-created_at,name |
- : descending |
skip=20&limit=10 |
paging : add a sort |
getTotalCount=true |
adds totalCount, before paging |
deep=[{s_key:'category'}] |
the related rows, in the same call |
customer_id=42 · price>=100 · name=Bob,Alice |
any column is a filter : > >= < <= !=, lists |
find={'owner.customer_id':42} |
find and join : a field of a related table |
upsert=true |
update or replace by id : insert an unknown id |
returnDocument=before |
update or replace by id : answer the row before |
throwErrorIfRecordNotFound=true |
update, replace or remove by id : 404 instead of data: null |
The query APIs take the same keys in their body.
find operators find
|
|
| Compare |
$eq $ne $gt $gte $lt $lte $in $nin |
| Logic |
$and $or $nor $not |
| Element |
$exists · $type MongoDB |
| Text |
$like: 'Bob%' · $regex with $options · $text MongoDB |
| Others |
$mod $expr · $isNull SQL · $where $jsonSchema MongoDB |
| Arrays MongoDB |
$all $elemMatch $size |
| Bits |
$bitsAllSet $bitsAnySet $bitsAllClear $bitsAnyClear |
| Geo MongoDB |
$geoWithin $geoIntersects $near $nearSphere |
SQL databases get the same filter, translated. $like : % is any text, _ one character, on every database.
deep: [{
s_key: 'customer_id', // the field here
t_col: 'customers', // the other table
t_key: 'id', // default : its key
t_db, t_instance, // when they differ
isMultiple: true, // an array of rows
find, select, sort, skip, limit,
deep: [ … ], // the next level
}]
With a relation in the schema, s_key alone is enough. One query per deep item, for all the rows.
What a write answers returnData
|
|
| Save, master save |
the saved rows ; with ?returnData=false, only { savedRowsCount } |
| Update many |
{ updatedRowsCount } ; with "returnData": true, also updatedRows |
| Remove by query |
{ deletedRowsCount } ; with "returnData": true, also deletedRows and ids |
From code, returnData is a key of the call (return_data in Python and Rust).
{ "success": true, "statusCode": 200,
"data": [], "totalCount": 5,
"warnings": [], "logs": [], "meta": {} }
{ "success": false, "statusCode": 400,
"errors": [ { "type": "required",
"field": "name", "message": "…",
"code": 400, "dataIndex": 0 } ] }
|
|
data |
the rows, the row, or what your code returns |
logs |
the g.logger lines of the code which ran |
meta |
with x-am-meta: true, admin panel only |
|
|
200 |
done |
201 |
created : save, master save |
400 |
wrong request : validation, bad query, no body |
401 |
token missing, malformed or expired |
403 |
the token is fine, no group grants it ; or the origin is not allowed |
404 |
no such path ; an unknown id with throwErrorIfRecordNotFound |
500 |
an error of your code or of the database |
| Response header |
|
x-am-data-source |
cache or api |
x-am-request-id |
the id of the request in the logs |
content-encoding |
gzip, br or deflate |
|
|
AM_00001 |
a field is missing or of the wrong type |
AM_00034 |
403 : no group of the API user grants the API |
AM_00035 |
401 : x-am-authorization is not valid |
AM_00092 |
the person token is missing |
AM_00056 |
no schema on the table : call /api/gen |
AM_00117 |
the row changed since you read it : version mismatch |
AM_00119 |
record not found, with throwErrorIfRecordNotFound |
AM_00032 |
the origin of the browser is not allowed |
Every message comes in the language of x-am-internationalization.
import {
EType, ISchemaType, ISchemaProperty,
} from 'types';
const schema: ISchemaType = {
name: EType.string, // a type alone
price: <ISchemaProperty>{
__type: EType.number,
validations: { required: true, min: 0 },
},
tags: [EType.string], // an array
address: { city: EType.string },
};
module.exports = { schema };
Only the schema APIs read it. Nested objects and arrays are for MongoDB : SQL tables are flat. A key the schema does not have is refused.
__type |
Holds |
EType.string |
text ; also a JSON column |
EType.number |
numbers |
EType.boolean |
true, false |
EType.date |
dates, ISO strings in JSON |
EType.objectId |
a MongoDB ObjectId |
EType.file · EType.files |
uploads, in the request schema of a custom API |
[EType.number] |
a vector column |
| Other values |
In JSON |
| Vector |
[0.12, 0.5, 0.33] |
| Bytes |
{ "type": "Buffer", "data": [1, 2] } |
| Shape |
GeoJSON in ; the form of the driver out |
| Interval |
{ "days": 1, "hours": 2 } |
| PostgreSQL array |
[1, 2, 3] |
|
|
trim trimStart trimEnd |
removes the spaces around |
toLowerCase toUpperCase |
changes the case |
conversionFun: (value, row) => … |
your function : it runs even when the field is absent |
defaults: { defaultValue } |
a value for a missing field |
defaults: { defaultFun: () => … } |
the same, computed ; defaultValue wins |
shouldReplaceNullWithDefault · shouldReplaceEmptyStringWithDefault |
in defaults : null or "" get the default too |
encryption: true |
stored encrypted (AES, keys of the secret) |
hashing: true |
stored as an HMAC SHA-256 hash |
The keys of conversions. They run before the validations, on save and on update.
validations |
Error type |
required: true |
required |
min · max |
min · max |
minLength · maxLength |
minLength · maxLength |
email: true |
emailNotValid |
enum: [ … ] |
enumValidation |
unique: true |
unique |
validatorFun: (value, row) => true |
invalidValue |
A refused save answers 400 with every problem at once : { type, field, message, code, dataIndex }.
Keys and generated values Schema
|
|
isPrimaryKey: true |
the id of get, update and remove by id |
isAutoIncrementByDB: true |
the database counts (SQL identity) |
isAutoIncrementByAM: { start, step } |
API Maker counts, on every database ; true is 1, 1 |
isAutoGenerateByAM: { valueGeneratorType } |
ObjectID GUID_UUID ULID ShortUUID |
isConcurrencyControlField: true |
the version field : a stale update gets 400 |
// orderItems.order_id → orders._id
order_id: <ISchemaProperty>{
__type: EType.objectId,
database: 'shop', collection: 'orders',
column: '_id',
},
// orders.items : its rows of orderItems
items: <ISchemaProperty>{
__type: EType.objectId,
isVirtualField: true,
database: 'shop',
collection: 'orderItems',
s_columnVirtualLinker: '_id',
t_columnVirtualLinker: 'order_id',
},
Add instance for another instance ; table works as collection. Relations feed deep, master save and the ER diagram.
// Basic info : TypeScript in every language
let customApi: T.ICustomApiSettingsTypes = {
name: 'Hello World',
path: '/hello-world',
requestMethod: T.ERequestMethod.GET,
apiAccessType:
T.EAPIAccessType.TOKEN_ACCESS,
errorList: [],
};
module.exports = customApi;
It answers at /api/custom-api/<user-path>/hello-world, with the exact path and method. What main returns is data.
main in every language Languages
|
|
| TypeScript |
async function main(g: T.IAMGlobal) then module.exports = main; |
| Python |
async def main(g: G): |
| PHP |
function main(AmG $g) |
| C# |
public static async Task<object?> Main(G g) in file static class Api |
| Go |
func Main(g *am.G) (any, error) in package code |
| Rust |
pub fn main(g: &mut G) -> Result<Value, Errors> |
| Java |
static Object main(G g) in class Api |
| Kotlin |
fun main(g: G): Any? |
Hooks, listeners, schedulers and initializers are the same function.
|
|
| TypeScript |
throw [{ code: 400, message }] |
| Python |
raise Errors(message, code=400) |
| PHP |
throw new AmErrors($message, 400) |
| C#, Java |
throw new AmErrors(message, 400) |
| Kotlin |
throw AmErrors(message, 400) |
| Go |
return nil, am.Fail(400, message) |
| Rust |
Err(am::fail(400, message)) |
The answer : success: false, the message in errors, the code as status. Any other error answers 500. To send your own data with a status, set g.res.statusCode.
Custom API settings Settings
|
|
reqBodySchema · reqQueryParametersSchema |
converted and checked before the code runs |
enableCaching |
the answer is kept in Redis |
resetCacheOnModificationOf |
'DB:instance:database:table' · 'CA:name' |
authProviders |
the person tokens it needs ; [] : the API user only |
acceptOnlyEncryptedData |
plain bodies are refused |
customApiTimeoutInSeconds |
time of the code and its hooks ; 13 s by default |
runOnNativeProcess |
runs on the machine of the server, not in Docker |
separateSandboxSettings |
a sandbox of its own |
fileUpload |
enable, the fields, sizes and extensions |
errorList |
your messages, for i18N |
Files and other answers Files
Upload : multipart/form-data, field files (up to files41). The code gets g.req.body.files : originalname, mimetype, size, path. 512 MiB per form by default.
Download : return the __am__ keys.
return {
__am__downloadFilePath: 'r.pdf',
__am__downloadFolderFileName: 'May.pdf',
__am__cleanupFileOrFolderPaths: 'r.pdf',
};
Several paths in __am__downloadFileOrFolderPaths : one zip. Other content : set g.res.contentType and return the text, or bytes as base64. Headers of the answer : g.res.headers.
|
|
| Utility class |
code your other code imports : import * as utils from 'utils/MyClass' in TypeScript |
| Process initializer |
runs when a sandbox process starts, before any API : subscribe, warm a cache |
| Migration script |
changes a database once on each environment, after a Git pull |
g.shared |
values of one runner, for all its requests |
g.res.shared |
values of one request : pre hook → API → post hook |
|
|
| Where |
an instance, a database, a table, one API, a custom or a system API |
| Order |
pre : instance → database → table → API. Post : the other way. |
groupNames |
runs only for API users of these groups |
| Language |
the language of its API |
| From code |
g.sys.db calls skip the hooks ; skipHookRunning: false runs them |
| Cache |
an answer from the cache runs no hook |
| A pre hook |
Then |
changes g.req.query, body, headers |
the API gets the change |
| returns a value |
it is the answer : the API does not run |
| throws |
the call is refused |
| A post hook |
Then |
changes g.res.output |
the answer changes |
| returns a value |
it is the answer ; later post hooks do not run |
| throws |
an error answer ; the write stays done |
g.req.isApiRequestFromUser is false for a call of your own code.
import * as assert from 'node:assert';
module.exports = [{
name: 'orderCount counts the orders',
mocks: [{ method: 'db.getAll',
when: [{ collection: 'orders' }],
return: [{ id: 1 }, { id: 2 }] }],
code: async (g: T.IAMGlobal) => {
const out = await g.sys.test
.runCustomApi({ name: 'orderCount' });
assert.deepStrictEqual(out, { count: 2 });
},
}];
npm run test in the folder of the local client : exit 0 passed, 1 failed, 2 not complete. The coverage goes to coverage/lcov.info. On the page, Cmd+E or Ctrl+E in the editor runs it.
// every listener of the event
await g.sys.system.emitEvent('order-placed',
{ order_no: 1001 });
// only these listeners
await g.sys.system.emitEvent('order-placed',
{ order_no: 1001 }, ['send-invoice']);
// in a listener
const data = g.req.eventData;
Over HTTP : POST …/system-api/<user-path>/emit-event with { name, eventData, executeListeners }. Or by itself after an API : Automatically trigger on API hit. The emitter gets outputArr, the answer of each listener. A loop of events is stopped.
|
|
| Intervals |
seconds, minutes, hours, days, working days, months, a year, or cron. Each one has its time zone. |
| Runs on |
one server, which hands over when it stops ; or every server, once per server or once per runner |
| Timeout |
10 minutes by default |
g.req.scheduler |
worker of workers, firedAt, serverIndex of servers, runnerIndex of runners |
| Run now |
API Testing : pick the scheduler, send |
| Stop |
the Active switch, on every server at once |
sec min hour day mon week
0 0 22 * * 1-5 22:00 Mon-Fri
0 15 14 1 * * 14:15 on the 1st
*/30 * * * * * every 30 seconds
0 0 */6 * * * every 6 hours
|
|
| sec, min |
0-59 |
| hour |
0-23 |
| day |
day of the month, 1-31 |
| mon |
1-12 or jan-dec |
| week |
day of the week, 0-7 or sun-sat : 0 and 7 are Sunday |
A field takes *, 5, 1-5, 1,15 or */10. Five fields : no seconds. Presets : @yearly @monthly @weekly @daily @hourly @minutely @secondly @weekdays @weekends.
const ws = new WebSocket('wss://host/'
+ '?x-am-authorization=' + token);
// once CONNECTED came :
ws.send(JSON.stringify({
objType: 'REGISTER',
onEvents: [{
eventType: 'INSTANCES',
apiName: 'SCHEMA_GET_ALL',
instance: 'mongodb', database: 'shop',
collection: 'orders',
getEventData: true,
}],
}));
| Message |
|
CONNECTED |
the first one : send REGISTER |
REGISTER |
validOnEvents with their eventId, invalidOnEvents with why |
NOTIFICATION |
{ eventId, eventData } after each successful call |
UNREGISTER |
send { objType: 'UNREGISTER', onEvents: [eventId] } |
TOKEN_VALIDATION |
a token is wrong or ran out : then close 1000 |
| Key |
|
eventType |
INSTANCES CUSTOM_APIS SYSTEM_APIS CUSTOM_WS_EVENTS |
apiName |
an API id, a custom API, a system API id, or an event name |
condition |
{ conditionType: 'RESPONSE', criteria: { recipientId: 42 } } |
select |
{ order_no: 1 } : the fields to send |
getEventData |
false : only the fact that it happened |
|
|
| Emit |
g.sys.system.emitEventWS(name, data), or /emit-event-ws |
Close 1000 |
the tokens no longer let it in |
Close 1001 |
the server stops : connect and REGISTER again |
| Who |
groups grant the events ; subscriptions end with the connection |
const lock = await g.sys.system.getLock(
{ name: `invoice-${id}`, ttlSeconds: 30 });
if (!lock.acquired) return { sent: false };
try {
// the work, one run at a time
} finally {
await g.sys.system.releaseLock(lock);
}
Options : ttlSeconds (60), waitSeconds, tries, retryDelayMs, maxHolders, throwIfNotAcquired. On every server, in every language. Code only : no HTTP API.
|
|
| Turn on |
enableCaching: true in the settings of a table, a database, a custom or a system API |
| Kept |
until a write through API Maker, at most 7200 s |
| Reset |
x-am-cache-control: reset_cache · the reset system APIs · g.sys.cache.resetCacheDB |
| Your keys |
g.sys.cache.setKey getKey removeKey |
| See it |
x-am-data-source: cache · the Redis Dashboard |
Each caller has its own cached answers : the tokens are part of the key.
| Who |
Header |
In g.req.auth |
| The app |
x-am-authorization |
authAMUser |
| Your users |
x-am-user-authorization |
authAMDB |
| Google |
x-google-authorization |
authGoogle |
| Azure AD |
x-azure-authorization |
authAzure |
| AWS Cognito |
x-aws-authorization |
authAWS |
| Your own |
x-custom-authorization |
authCustom |
Your code reads who calls in g.req.auth. Require a provider with authProviders: ['users_tg'] in the settings of an API, a table or a database, or in common.authProviders of the secret.
let dbTokenGenerator: T.IAuthTokenAMDB = {
name: 'users_tg',
instance: 'mongodb',
database: 'shop',
collection: 'users', // table: in SQL
usernameColumn: 'email',
passwordColumn: 'password',
groupsColumn: 'groups', // 'a,b' or '*'
expiresInSeconds: 259200,
};
module.exports = dbTokenGenerator;
Security → Auth Providers. A person signs in with { "name": "users_tg", "u", "p" } on /token.
|
|
hashingAlgorithm nonce |
hashing: true, hash data |
encryptionAlgorithm secret |
encryption: true (AES) |
encryptionAlgorithmFETransfer secretFETransfer |
encrypt and decrypt data, encrypted payloads : the key of your apps |
feTransferDataValidityInSeconds |
how old an encrypted payload may be |
apiUserPasswords |
passwords of API users, by path |
connectionString |
the strings the instance form offers |
authProviders |
the providers every API needs |
multiTenant |
the tenants tables |
The keys of common. From code : g.sys.system.getSecret(['common.secret']). A secret never goes to Git.
|
|
| APIs and WebSockets |
38246 (48246 without Launchpad) |
| Admin panel, local |
4626 : http://127.0.0.1:4626 |
| MongoDB of API Maker |
38248 |
| Redis of API Maker |
7479 |
| Your databases, local |
MongoDB 27018 · PostgreSQL 5433 · MySQL 3307 · MariaDB 3308 · Percona 3309 · TiDB 4001 · SQL Server 14330 · Oracle 1522 |
Launchpad picks free ports for a new project : these are the defaults.
|
|
| Time of the code |
13000 ms |
| Time of a scheduler |
10 minutes |
| A token |
259200 s (3 days) ; refresh 900 s more |
| A cached answer |
7200 s, up to 1,000,000 characters |
| Compression |
answers from 51200 bytes |
| A body · a form with files |
16 MiB · 512 MiB |
| Uploaded files kept |
1800 s |
| A lock |
ttlSeconds 60 |
|
|
| The page |
Settings → Deployment Settings, root user : every server at once |
.env of a server |
wins over the page : am__port=48246 |
| Command line |
--am.port=48246 ; --envPath=<file> |
| Required |
mongo_db_connection passCommunication passJWT passDBEncryptDecrypt redisInternal.nodes |
| Public address |
BE_HOST_PORT=https://api.example.com |
| Log |
AM_LOG=debug, or info,am_sandbox=debug |
|
|
| MongoDB |
mongodb://user:pass@host:27017/?authSource=admin · mongodb+srv://user:pass@cluster/ |
| MySQL, Percona, TiDB |
mysql://user:pass@host:3306/db |
| MariaDB |
mariadb://user:pass@host:3306/db |
| PostgreSQL |
postgresql://user:pass@host:5432/db?sslmode=require |
| SQL Server |
Server=host,1433;Database=db;User Id=u;Password=p;TrustServerCertificate=True; |
| Oracle |
host:1521/service, with a username key and a password key |
The string stays in the secret : the instance names its key. Write the password as it is.
AGENTS.md package.json tsconfig.json
src/
custom_apis/orders/
orders.yaml the item
orders.ts its code : .py .go …
orders.config.ts its settings
pre-hooks/check_stock.ts
schedulers/ events/ schemas/
collection_hooks/ utility_classes/
test_cases/ admin_user/data.yaml
Commit on the Git page. Pull on another server : it also runs the new migration scripts. Secrets never go to Git.
|
|
npm run start |
keeps the folder in sync, both ways |
npm run test |
runs the test cases ; --no-sync, --push |
local-client.config.js |
the sync token : not in Git |
| A CI job |
AM_WEB_SOCKET_URL AM_SYNC_TOKEN AM_ADMIN_USER_PATH |
Up to 50 local clients on one account. Nothing is committed for you.
- Menu of your user → Debug Settings → Debugging on.
- API Testing opens on Debug. Click left of a line, or F9 : a breakpoint.
- Right click the margin : a condition, a hit count, a logpoint.
- Another client sends
x-am-run-in-debugger: true.
Every language, in the sandboxes and on the native process.
|
|
pages/**/*.tsx |
a page : default, loader, action, config, head |
pages/**/*.ts |
an endpoint : GET POST PUT PATCH DELETE ALL |
_layout.tsx _middleware.ts |
the layout, the middleware |
_404.tsx _error.tsx |
the pages of errors |
site.config.ts |
defineSite({ … }) |
In a page, ctx has db, api, cache, auth, cookies, fetch. The module am-ssr has redirect, notFound, json, formatDate, paginate…