diff --git a/frontend-integration/01-totaljs-routing.md b/frontend-integration/01-totaljs-routing.md index 0da0888..60395b4 100644 --- a/frontend-integration/01-totaljs-routing.md +++ b/frontend-integration/01-totaljs-routing.md @@ -23,18 +23,18 @@ DELETE /posts/abc123 → delete post **Total.js API Routing:** ``` -POST /api/ { "schema": "posts_list" } -POST /api/ { "schema": "posts_read/abc123" } -POST /api/ { "schema": "posts_create", "data": { ... } } -POST /api/ { "schema": "posts_update/abc123", "data": { ... } } -POST /api/ { "schema": "posts_remove/abc123" } +POST /api/ { "schema": "Posts|list" } +POST /api/ { "schema": "Posts|read", "data": { "id": "abc123" } } +POST /api/ { "schema": "Posts|create", "data": { ... } } +POST /api/ { "schema": "Posts|update", "data": { "id": "abc123", ... } } +POST /api/ { "schema": "Posts|remove", "data": { "id": "abc123" } } ``` | Concern | REST | Total.js API Routing | |---------|------|----------------------| | HTTP endpoint count | One per resource | **One for everything** | | HTTP verbs | GET, POST, PUT, DELETE | **Always POST** | -| Resource identity | In the URL path | In the `schema` string | +| Resource identity | In the URL path | In the `data` object | | Action | In the HTTP verb | In the `schema` string | | Query params | On the URL | Appended to the `schema` string | | Request body | Payload only | `{ schema, data }` envelope | @@ -56,11 +56,23 @@ x-token: ← omitted for public schemas This is the only URL your client calls for **JSON API work**. File upload/download, health checks, and WebSockets are ordinary HTTP/WS routes and are documented separately. Do not invent a REST surface for CRUD. -The path is project-defined. Many backends use `/api/`. Some register API routing on the root path: +The path is project-defined. Many backends use `/api/`. An action can register API routing on the root path: ```javascript -ROUTE('API / +account_login --> Auth/login'); -ROUTE('+API / -account_logout --> Auth/logout'); +NEWACTION('Account|login', { + input: '*email,*password', + route: 'API /', + action: function($, model) { + // Public login action + } +}); + +NEWACTION('Account|logout', { + route: '+API /', + action: function($) { + // Protected logout action + } +}); ``` The client then calls `POST https://api.example.com/` with the same `{ schema, data }` envelope. Make the API path configurable instead of hard-coding `/api/`. @@ -69,53 +81,60 @@ The client then calls `POST https://api.example.com/` with the same `{ schema, d ## The schema string -The schema string is the complete address of an operation. It has up to three parts: +The schema string identifies a stable backend action. Input values such as IDs belong in `data`: ``` -_ -_/ -_/?key=value&key=value +| +|?key=value&key=value ``` ### Part 1 — Resource -The domain entity being acted upon: `account`, `posts`, `users`, `orders`, `messages`, etc. This mirrors your backend schema definition name. +The action namespace being addressed: `Posts`, `Users`, `Orders`, `Messages`, etc. ### Part 2 — Action A verb describing what to do. Common conventions: -| Action suffix | Meaning | +| Action name | Meaning | |---------------|---------| -| `_list` | Return all records (optionally filtered) | -| `_read` | Return one record by ID | -| `_create` | Create a new record | -| `_insert` | Alias for create (used interchangeably) | -| `_update` | Update an existing record | -| `_remove` | Delete a record | -| `_toggle_` | Toggle a boolean field | -| `_search` | Free-text or semantic search | -| `_export` | Export data | -| `_import` | Import data | +| `\|list` | Return all records (optionally filtered) | +| `\|read` | Return one record by ID | +| `\|create` | Create a new record | +| `\|insert` | Alias for create (used interchangeably) | +| `\|update` | Update an existing record | +| `\|remove` | Delete a record | +| `\|toggle_` | Toggle a boolean field | +| `\|search` | Free-text or semantic search | +| `\|export` | Export data | +| `\|import` | Import data | Custom actions beyond CRUD are common and encouraged — they make intent explicit: ``` -account_logout -account_password ← change password -account_verify ← verify email -session_refresh -notifications_mark_read +Account|logout +Account|password ← change password +Account|verify ← verify email +Session|refresh +Notifications|mark_read ``` -### Part 3 — Dynamic segment (optional) +### Input parameters -A resource ID or other path parameter appended after a `/`: +Declare required action input on the backend and send it in `data`: +```javascript +NEWACTION('Posts|read', { + input: '*id', + route: '+API /api/', + action: function($, model) { + // model.id + } +}); ``` -posts_read/abc123 -posts_update/abc123 -users_remove/usr_9f4k2 + +```json +{ "schema": "Posts|read", "data": { "id": "abc123" } } ``` ### Query parameters (optional) @@ -123,8 +142,8 @@ users_remove/usr_9f4k2 Appended directly to the schema string — **not** to the HTTP URL: ```json -{ "schema": "posts_list?page=2&limit=20&status=published" } -{ "schema": "posts_search?limit=10&mode=semantic", "data": { "query": "..." } } +{ "schema": "Posts|list?page=2&limit=20&status=published" } +{ "schema": "Posts|search?limit=10&mode=semantic", "data": { "query": "..." } } ``` --- @@ -149,7 +168,7 @@ You do not need to explore or version an endpoint tree. The backend developer te **5. Actions are self-documenting.** -`orders_cancel/ord_123` is unambiguous. `DELETE /orders/ord_123` requires knowledge that DELETE means "cancel" in this context and not "archive" or "refund". +`Orders|cancel` with `{ "id": "ord_123" }` in `data` is unambiguous. `DELETE /orders/ord_123` requires knowledge that DELETE means "cancel" in this context and not "archive" or "refund". --- @@ -175,19 +194,19 @@ Frontend auth behavior should be documented separately by the backend team or ve ## Schema naming conventions summary ``` -account_login public auth -account_logout protected auth -account get current user profile (protected) -account_update update profile (protected) +Account|login public auth +Account|logout protected auth +Account|read get current user profile (protected) +Account|update update profile (protected) -posts_list list (protected or public depending on backend) -posts_read/{id} read one -posts_create create -posts_update/{id} update -posts_remove/{id} delete +Posts|list list (protected or public depending on backend) +Posts|read read one; send id in data +Posts|create create +Posts|update update; send id in data +Posts|remove delete; send id in data -posts_list?page=2 pagination via query param in schema -posts_search?limit=5 search with filter params +Posts|list?page=2 pagination via query param in schema +Posts|search?limit=5 search with filter params ``` Conventions may vary slightly per project. The backend developer defines the schema names — this guide describes the patterns used in a standard Total.js API Routing setup. diff --git a/frontend-integration/02-request-response.md b/frontend-integration/02-request-response.md index d34628c..b141643 100644 --- a/frontend-integration/02-request-response.md +++ b/frontend-integration/02-request-response.md @@ -11,7 +11,7 @@ POST https://totaljsbackend.com/api/ Always use one API endpoint. The path is project-specific: - Generic examples often use `POST /api/` -- Projects with `ROUTE('API / ...')` use `POST /` +- Actions declared with a root `route: 'API /'` use `POST /` Keep the path configurable, for example with `EXPO_PUBLIC_API_PATH`. @@ -37,30 +37,30 @@ Do not attach tokens to known public schemas if the app can avoid it. Keep a cli | Field | Type | Required | Description | |-------|------|----------|-------------| -| `schema` | string | Always | The operation address. May include `/{id}` and `?query=params`. | -| `data` | object | When the operation needs input | Omit entirely for operations that take no parameters (e.g. list, read). | +| `schema` | string | Always | The stable action name. May include `?query=params`. | +| `data` | object | When the operation needs input | Contains declared inputs such as `id`; omit only when the action needs no input. | ### Examples **List — no data:** ```json -{ "schema": "posts_list" } +{ "schema": "Posts|list" } ``` **List with pagination — params in schema string:** ```json -{ "schema": "posts_list?page=2&limit=20&status=published" } +{ "schema": "Posts|list?page=2&limit=20&status=published" } ``` -**Read one — ID in schema string:** +**Read one — ID in data:** ```json -{ "schema": "posts_read/abc123" } +{ "schema": "Posts|read", "data": { "id": "abc123" } } ``` **Create — payload in data:** ```json { - "schema": "posts_create", + "schema": "Posts|create", "data": { "title": "Hello World", "body": "Content here", @@ -69,23 +69,23 @@ Do not attach tokens to known public schemas if the app can avoid it. Keep a cli } ``` -**Update — ID in schema, fields in data:** +**Update — ID and fields in data:** ```json { - "schema": "posts_update/abc123", - "data": { "title": "Updated Title", "status": "published" } + "schema": "Posts|update", + "data": { "id": "abc123", "title": "Updated Title", "status": "published" } } ``` -**Delete — no data:** +**Delete — ID in data:** ```json -{ "schema": "posts_remove/abc123" } +{ "schema": "Posts|remove", "data": { "id": "abc123" } } ``` **Search — params in schema, query in data:** ```json { - "schema": "posts_search?limit=10&mode=semantic", + "schema": "Posts|search?limit=10&mode=semantic", "data": { "query": "how to get started" } } ``` @@ -95,7 +95,7 @@ Do not attach tokens to known public schemas if the app can avoid it. Keep a cli Some Total.js projects support `GET /?schema=` for read-only helper calls. Treat this as project-specific convenience. The durable contract remains the JSON envelope: ```text -GET /?schema=posts_list%3Flimit%3D20 +GET /?schema=Posts%7Clist%3Flimit%3D20 ``` --- @@ -155,13 +155,13 @@ Production clients should centralize this normalization: | Operation | `value` | |-----------|---------| -| `*_list` | Array of records | -| `*_read/{id}` | Single record object | -| `*_create` / `*_insert` | Created record or its ID | -| `*_update/{id}` | Updated record | -| `*_remove/{id}` | `true` or `null` | -| `account_login` | Session token string (or nested in response root as `token`) | -| `account` (profile) | Current user object | +| `*\|list` | Array of records | +| `*\|read` | Single record object | +| `*\|create` / `*\|insert` | Created record or its ID | +| `*\|update` | Updated record | +| `*\|remove` | `true` or `null` | +| `Account\|login` | Session token string (or nested in response root as `token`) | +| `Account\|read` (profile) | Current user object | ### Paginated response @@ -252,7 +252,7 @@ Step 1 — Upload file: Step 2 — Register in the app: POST https://totaljsbackend.com/api/ - { "schema": "documents_create", "data": { id, url, name, size, type } } + { "schema": "Documents|create", "data": { id, url, name, size, type } } ``` ### Upload response shape diff --git a/frontend-integration/03-authentication.md b/frontend-integration/03-authentication.md index 895261f..7357b44 100644 --- a/frontend-integration/03-authentication.md +++ b/frontend-integration/03-authentication.md @@ -11,7 +11,7 @@ There are no cookies, no JWTs with decodable claims, no refresh tokens in the st ## Token lifecycle ``` -1. POST /api/ or / { schema: "account_login", data: { email, password } } +1. POST /api/ or / { schema: "Account|login", data: { email, password } } ↓ 2. Server returns session token ↓ @@ -23,7 +23,7 @@ There are no cookies, no JWTs with decodable claims, no refresh tokens in the st ↓ 6. On 401 → clear stored token → redirect to login screen ↓ -7. On logout: POST /api/ or / { schema: "account_logout" } → clear stored token +7. On logout: POST /api/ or / { schema: "Account|logout" } → clear stored token ``` The 401 handler lives in the HTTP client interceptor, runs globally, and requires no per-call handling. @@ -39,7 +39,7 @@ On React Native, keep the token in `expo-secure-store`. Persisted Zustand/MMKV s ```json POST /api/ { - "schema": "account_login", + "schema": "Account|login", "data": { "email": "user@example.com", "password": "hunter2" @@ -73,7 +73,7 @@ Mobile projects can expose a dedicated mobile login schema: ```json POST /api/ { - "schema": "account_login_mobile", + "schema": "Account|login_mobile", "data": { "phone": "+22600000000", "country": "BF", @@ -112,7 +112,7 @@ if item.success === true → store item.token, set user = item.value, navigate ```json POST /api/ { - "schema": "account_create", + "schema": "Account|create", "data": { "name": "Jane Doe", "email": "user@example.com", @@ -130,9 +130,9 @@ POST /api/ } ``` -`value` is the session token string directly. Store it and consider the user authenticated. Then call `account` to hydrate the user object. +`value` is the session token string directly. Store it and consider the user authenticated. Then call `Account|read` to hydrate the user object. -Mobile registration may use `account_create_mobile` and include fields such as `phone`, `country`, `language`, `terms`, `type`, `firstname`, or `lastname`. +Mobile registration may use `Account|create_mobile` and include fields such as `phone`, `country`, `language`, `terms`, `type`, `firstname`, or `lastname`. --- @@ -144,7 +144,7 @@ Use this on app startup to check whether a stored token is still valid and to po POST /api/ x-token: -{ "schema": "account" } +{ "schema": "Account|read" } ``` ### Success response @@ -172,7 +172,7 @@ If this returns HTTP `401`, the token is expired. Clear it and redirect to login POST /api/ x-token: -{ "schema": "account_logout" } +{ "schema": "Account|logout" } ``` Always clear the stored token client-side regardless of the response. If the server call fails (network error), the token is still cleared — the user is logged out locally. @@ -192,7 +192,7 @@ Restore non-sensitive preferences (language, country, city) ↓ Read token from secure storage ├── No token → show public/guest shell, done - └── Token present → POST /api/ or / { schema: "account" } + └── Token present → POST /api/ or / { schema: "Account|read" } ↓ success? ├── Yes → set user in state, hydrate memberships, show app @@ -214,7 +214,7 @@ POST /api/ x-token: { - "schema": "account_password", + "schema": "Account|password", "data": { "current_password": "hunter2", "new_password": "c0rrect-horse" @@ -227,19 +227,19 @@ x-token: ```json POST /api/ { - "schema": "account_reset", + "schema": "Account|reset", "data": { "email": "user@example.com" } } ``` The server emails a reset link with a time-limited token. -Some projects use `account_password` for the reset request and reserve `account_password_reset` for submitting the new password: +Some projects use `Account|password` for the reset request and reserve `Account|password_reset` for submitting the new password: ```json POST /api/ { - "schema": "account_password_reset", + "schema": "Account|password_reset", "data": { "token": "", "password": "new-password", @@ -253,7 +253,7 @@ POST /api/ ```json POST /api/ { - "schema": "account_verify", + "schema": "Account|verify", "data": { "token": "" } } ``` @@ -266,7 +266,7 @@ POST /api/ ``` Step 1 — Get OAuth redirect URL from backend: - POST /api/ { "schema": "account_google?page=dashboard" } + POST /api/ { "schema": "Account|google?page=dashboard" } Response: { "success": true, "value": "https://accounts.google.com/o/oauth2/..." } Step 2 — Redirect browser to the OAuth provider URL @@ -274,7 +274,7 @@ Step 2 — Redirect browser to the OAuth provider URL Step 3 — Provider redirects back to your app with a session ID Step 4 — Exchange session ID for a backend session token: - POST /api/ { "schema": "account_oauth", "data": { "sessionid": "" } } + POST /api/ { "schema": "Account|oauth", "data": { "sessionid": "" } } Response: { "success": true, "token": "..." } ``` @@ -285,7 +285,7 @@ When the mobile app obtains the OAuth ID token directly using the platform SDK ( ```json POST /api/ { - "schema": "account_login_google", + "schema": "Account|login_google", "data": { "token": "" } } ``` @@ -293,7 +293,7 @@ POST /api/ ```json POST /api/ { - "schema": "account_login_github", + "schema": "Account|login_github", "data": { "token": "" } } ``` @@ -304,9 +304,9 @@ Some mobile backends also expose: | Schema | Purpose | |--------|---------| -| `account_login_facebook` | Exchange a Facebook mobile token for a backend session. | -| `account_oauth_mobile` | Exchange a mobile OAuth session id for a backend session. | -| `account_google` / `account_facebook` | Start a provider flow and return redirect or session metadata. | +| `Account\|login_facebook` | Exchange a Facebook mobile token for a backend session. | +| `Account\|oauth_mobile` | Exchange a mobile OAuth session id for a backend session. | +| `Account\|google` / `Account\|facebook` | Start a provider flow and return redirect or session metadata. | --- @@ -316,11 +316,11 @@ OTP flows are usually public until a code is exchanged for an authenticated acti | Schema | Auth | Purpose | |--------|------|---------| -| `otp_sms` | Public | Send an SMS code. | -| `otp_sms_verify` | Public | Verify an SMS code. | -| `otp_sms_verify_mobile` | Public | Verify SMS code for a mobile auth flow. | -| `otp_email` | Public | Send an email code. | -| `otp_email_verify` | Public | Verify an email code. | +| `OTP\|sms` | Public | Send an SMS code. | +| `OTP\|sms_verify` | Public | Verify an SMS code. | +| `OTP\|sms_verify_mobile` | Public | Verify SMS code for a mobile auth flow. | +| `OTP\|email` | Public | Send an email code. | +| `OTP\|email_verify` | Public | Verify an email code. | Keep these schemas in the anonymous allowlist so a stale local token does not change their behavior. @@ -335,7 +335,7 @@ Standard Time-based One-Time Password (TOTP), compatible with Google Authenticat **Step 1 — Generate secret:** ```json POST /api/ (x-token required) -{ "schema": "account_2fa_generate" } +{ "schema": "Account|2fa_generate" } ``` Response contains a `qr_uri` (to render as a QR code) and a `secret` (for manual entry). Show both to the user. @@ -343,7 +343,7 @@ Response contains a `qr_uri` (to render as a QR code) and a `secret` (for manual ```json POST /api/ (x-token required) { - "schema": "account_2fa_enable", + "schema": "Account|2fa_enable", "data": { "token": "123456" } } ``` @@ -352,17 +352,17 @@ POST /api/ (x-token required) ```json POST /api/ (x-token required) -{ "schema": "account_2fa_disable" } +{ "schema": "Account|2fa_disable" } ``` ### Verify TOTP during login (if 2FA is enabled) -After the initial `account_login` succeeds and you have a session token, the session may be pending 2FA verification. Verify it: +After the initial `Account|login` succeeds and you have a session token, the session may be pending 2FA verification. Verify it: ```json POST /api/ (x-token: pending session token) { - "schema": "account_2fa_verify", + "schema": "Account|2fa_verify", "data": { "token": "123456" } } ``` diff --git a/frontend-integration/04-api-reference.md b/frontend-integration/04-api-reference.md index c8abf12..b159205 100644 --- a/frontend-integration/04-api-reference.md +++ b/frontend-integration/04-api-reference.md @@ -12,7 +12,7 @@ POST https://totaljsbackend.com/api/ Replace `totaljsbackend.com` with the actual backend hostname for your project. -If the backend declares routes with `ROUTE('API / ...')`, the API endpoint is the root path: +If an action declares `route: 'API /'`, the API endpoint is the root path: ```text POST https://api.example.com/ @@ -32,17 +32,17 @@ Keep the API path configurable in clients. ## Standard CRUD schema patterns -Every resource follows this pattern. Replace `{resource}` with the actual name (`posts`, `users`, `orders`, `messages`, etc.): +Every resource follows this pattern. Replace `Namespace` with the actual action namespace (`Posts`, `Users`, `Orders`, `Messages`, etc.): | Schema | Auth | Data | Description | |--------|------|------|-------------| -| `{resource}_list` | 🔒 | — | Return all records | -| `{resource}_list?page=&limit=` | 🔒 | — | Paginated list | -| `{resource}_read/{id}` | 🔒 | — | Return one record | -| `{resource}_create` | 🔒 | Resource fields | Create a record | -| `{resource}_insert` | 🔒 | Resource fields | Alias for create | -| `{resource}_update/{id}` | 🔒 | Fields to update | Update a record | -| `{resource}_remove/{id}` | 🔒 | — | Delete a record | +| `Namespace\|list` | 🔒 | — | Return all records | +| `Namespace\|list?page=&limit=` | 🔒 | — | Paginated list | +| `Namespace\|read` | 🔒 | `id` | Return one record | +| `Namespace\|create` | 🔒 | Resource fields | Create a record | +| `Namespace\|insert` | 🔒 | Resource fields | Alias for create | +| `Namespace\|update` | 🔒 | `id` and fields to update | Update a record | +| `Namespace\|remove` | 🔒 | `id` | Delete a record | --- @@ -52,18 +52,18 @@ These are present in virtually every Total.js backend: | Schema | Auth | Data | Description | |--------|------|------|-------------| -| `account_create` | 🌐 | `name`, `email`, `password` | Register | -| `account_create_mobile` | 🌐 | `phone`, `country`, `password`, profile fields | Mobile registration | -| `account_login` | 🌐 | `email`, `password` | Login — returns session token | -| `account_login_mobile` | 🌐 | `phone` or `email`, `country`, `password` | Mobile login | -| `account_logout` | 🔒 | — | Invalidate session | -| `account` | 🔒 | — | Get current user profile | -| `account_update` | 🔒 | Profile fields | Update profile | -| `account_password` | 🔒 | `current_password`, `new_password` | Change password | -| `account_reset` | 🌐 | `email` | Request password reset | -| `account_password_reset` | 🌐 | `token`, `password`, `confirm` | Complete password reset | -| `account_verify` | 🌐 | `token` | Verify email address | -| `account_list` | 🔒⚡ | — | List all users (admin) | +| `Account\|create` | 🌐 | `name`, `email`, `password` | Register | +| `Account\|create_mobile` | 🌐 | `phone`, `country`, `password`, profile fields | Mobile registration | +| `Account\|login` | 🌐 | `email`, `password` | Login — returns session token | +| `Account\|login_mobile` | 🌐 | `phone` or `email`, `country`, `password` | Mobile login | +| `Account\|logout` | 🔒 | — | Invalidate session | +| `Account\|read` | 🔒 | — | Get current user profile | +| `Account\|update` | 🔒 | Profile fields | Update profile | +| `Account\|password` | 🔒 | `current_password`, `new_password` | Change password | +| `Account\|reset` | 🌐 | `email` | Request password reset | +| `Account\|password_reset` | 🌐 | `token`, `password`, `confirm` | Complete password reset | +| `Account\|verify` | 🌐 | `token` | Verify email address | +| `Account\|list` | 🔒⚡ | — | List all users (admin) | ### Login response ```json @@ -95,12 +95,12 @@ These are present in virtually every Total.js backend: | Schema | Auth | Data | Description | |--------|------|------|-------------| -| `account_google?page={page}` | 🌐 | — | Get Google OAuth redirect URL | -| `account_github?page={page}` | 🌐 | — | Get GitHub OAuth redirect URL | -| `account_oauth` | 🌐 | `sessionid` | Exchange OAuth session for token | -| `account_login_google` | 🌐 | `token` | Login with Google ID token (mobile) | -| `account_login_facebook` | 🌐 | `token` | Login with Facebook token (mobile) | -| `account_login_github` | 🌐 | `token` | Login with GitHub token (mobile) | +| `Account\|google?page={page}` | 🌐 | — | Get Google OAuth redirect URL | +| `Account\|github?page={page}` | 🌐 | — | Get GitHub OAuth redirect URL | +| `Account\|oauth` | 🌐 | `sessionid` | Exchange OAuth session for token | +| `Account\|login_google` | 🌐 | `token` | Login with Google ID token (mobile) | +| `Account\|login_facebook` | 🌐 | `token` | Login with Facebook token (mobile) | +| `Account\|login_github` | 🌐 | `token` | Login with GitHub token (mobile) | --- @@ -108,10 +108,10 @@ These are present in virtually every Total.js backend: | Schema | Auth | Data | Description | |--------|------|------|-------------| -| `account_2fa_generate` | 🔒 | — | Generate TOTP secret + QR URI | -| `account_2fa_enable` | 🔒 | `token` (6-digit TOTP) | Activate 2FA | -| `account_2fa_disable` | 🔒 | — | Deactivate 2FA | -| `account_2fa_verify` | 🔒 | `token` (6-digit TOTP) | Verify TOTP during login | +| `Account\|2fa_generate` | 🔒 | — | Generate TOTP secret + QR URI | +| `Account\|2fa_enable` | 🔒 | `token` (6-digit TOTP) | Activate 2FA | +| `Account\|2fa_disable` | 🔒 | — | Deactivate 2FA | +| `Account\|2fa_verify` | 🔒 | `token` (6-digit TOTP) | Verify TOTP during login | --- @@ -123,10 +123,10 @@ Typical public names: | Schema | Auth | Description | |--------|------|-------------| -| `account_login`, `account_create` | 🌐 | Password login and registration. | -| `account_reset`, `account_password_reset`, `account_verify` | 🌐 | Recovery and verification. | -| `posts_list`, `posts_read/{id}` | 🌐 | Public content list and detail. | -| `categories_list` | 🌐 | Public lookup list. | +| `Account\|login`, `Account\|create` | 🌐 | Password login and registration. | +| `Account\|reset`, `Account\|password_reset`, `Account\|verify` | 🌐 | Recovery and verification. | +| `Posts\|list`, `Posts\|read` | 🌐 | Public content list and detail. The read action receives `id` in `data`. | +| `Categories\|list` | 🌐 | Public lookup list. | Ask the backend team for the real public list. Do not assume names from another product. @@ -140,14 +140,14 @@ This is a fully worked example showing what a real Total.js backend schema set l | Schema | Auth | Data | Description | |--------|------|------|-------------| -| `posts_list` | 🌐 | — | List published posts | -| `posts_list?page=&limit=&status=` | 🔒 | — | Filtered/paginated list | -| `posts_read/{id}` | 🌐 | — | Read one post | -| `posts_create` | 🔒 | `title`, `body`, `status`, `tags` | Create post | -| `posts_update/{id}` | 🔒 | Any post fields | Update post | -| `posts_remove/{id}` | 🔒 | — | Delete post | -| `posts_publish/{id}` | 🔒 | — | Publish draft | -| `posts_search?limit=` | 🌐 | `query` | Full-text search | +| `Posts\|list` | 🌐 | — | List published posts | +| `Posts\|list?page=&limit=&status=` | 🔒 | — | Filtered/paginated list | +| `Posts\|read` | 🌐 | `id` | Read one post | +| `Posts\|create` | 🔒 | `title`, `body`, `status`, `tags` | Create post | +| `Posts\|update` | 🔒 | `id` and any post fields | Update post | +| `Posts\|remove` | 🔒 | `id` | Delete post | +| `Posts\|publish` | 🔒 | `id` | Publish draft | +| `Posts\|search?limit=` | 🌐 | `query` | Full-text search | **Post object:** ```json @@ -167,19 +167,19 @@ This is a fully worked example showing what a real Total.js backend schema set l | Schema | Auth | Data | Description | |--------|------|------|-------------| -| `comments_list?postid=` | 🌐 | — | List comments on a post | -| `comments_create` | 🔒 | `postid`, `body` | Add comment | -| `comments_update/{id}` | 🔒 | `body` | Edit own comment | -| `comments_remove/{id}` | 🔒 | — | Delete comment | +| `Comments\|list?postid=` | 🌐 | — | List comments on a post | +| `Comments\|create` | 🔒 | `postid`, `body` | Add comment | +| `Comments\|update` | 🔒 | `id`, `body` | Edit own comment | +| `Comments\|remove` | 🔒 | `id` | Delete comment | ### Categories | Schema | Auth | Data | Description | |--------|------|------|-------------| -| `categories_list` | 🌐 | — | List all categories | -| `categories_create` | 🔒⚡ | `name`, `slug` | Create category (admin) | -| `categories_update/{id}` | 🔒⚡ | `name`, `slug` | Update category (admin) | -| `categories_remove/{id}` | 🔒⚡ | — | Delete category (admin) | +| `Categories\|list` | 🌐 | — | List all categories | +| `Categories\|create` | 🔒⚡ | `name`, `slug` | Create category (admin) | +| `Categories\|update` | 🔒⚡ | `id`, `name`, `slug` | Update category (admin) | +| `Categories\|remove` | 🔒⚡ | `id` | Delete category (admin) | --- @@ -188,26 +188,26 @@ This is a fully worked example showing what a real Total.js backend schema set l ### Pagination ```json -{ "schema": "posts_list?page=2&limit=20" } +{ "schema": "Posts|list?page=2&limit=20" } ``` ### Filtering ```json -{ "schema": "posts_list?status=published&authorid=usr_xyz" } +{ "schema": "Posts|list?status=published&authorid=usr_xyz" } ``` ### Sorting ```json -{ "schema": "posts_list?sort=dtcreated&order=desc" } +{ "schema": "Posts|list?sort=dtcreated&order=desc" } ``` ### Search with options ```json { - "schema": "posts_search?limit=10&mode=fulltext", + "schema": "Posts|search?limit=10&mode=fulltext", "data": { "query": "total.js tutorial" } } ``` diff --git a/frontend-integration/05-integration-guide.md b/frontend-integration/05-integration-guide.md index 09b087a..737f0fa 100644 --- a/frontend-integration/05-integration-guide.md +++ b/frontend-integration/05-integration-guide.md @@ -85,7 +85,7 @@ export async function apiRequest(schema: string, data?: unknown): Promise { `getStoredToken`, `clearStoredToken`, and `redirectToLogin` are the only platform-specific parts of this layer. -Production mobile clients should make the endpoint path configurable. Generic examples often use `/api/`; Total.js projects that declare `ROUTE('API / ...')` use `/`. +Production mobile clients should make the endpoint path configurable. Generic examples often use `/api/`; actions declared with a root `route: 'API /'` use `/`. --- @@ -114,7 +114,7 @@ export type CreatePostInput = Pick; export const postsService = { list: (params?: { page?: number; limit?: number; status?: string }) => { - let schema = 'posts_list'; + let schema = 'Posts|list'; if (params) { const qs = new URLSearchParams( Object.entries(params) @@ -126,14 +126,14 @@ export const postsService = { return apiRequest(schema); }, - read: (id: string) => apiRequest(`posts_read/${id}`), - create: (data: CreatePostInput) => apiRequest('posts_create', data), - update: (id: string, data: Partial) => apiRequest(`posts_update/${id}`, data), - remove: (id: string) => apiRequest(`posts_remove/${id}`), - publish: (id: string) => apiRequest(`posts_publish/${id}`), + read: (id: string) => apiRequest('Posts|read', { id }), + create: (data: CreatePostInput) => apiRequest('Posts|create', data), + update: (id: string, data: Partial) => apiRequest('Posts|update', { ...data, id }), + remove: (id: string) => apiRequest('Posts|remove', { id }), + publish: (id: string) => apiRequest('Posts|publish', { id }), search: (query: string, options?: { limit?: number; mode?: string }) => { - let schema = 'posts_search'; + let schema = 'Posts|search'; if (options) { const qs = new URLSearchParams( Object.entries(options) @@ -237,7 +237,7 @@ function PostsPage() { ## Auth state — global, initialized on startup -Auth state lives in a global context or store that wraps the entire app. It runs one check on startup: read the stored token → call `account` → set user or clear token. +Auth state lives in a global context or store that wraps the entire app. It runs one check on startup: read the stored token → call `Account|read` → set user or clear token. ``` AppRoot @@ -279,7 +279,7 @@ Login (and sometimes other schemas) returns an array. Normalize at the service o ```typescript // In authService.login -const raw = await apiRequest('account_login', { email, password }); +const raw = await apiRequest('Account|login', { email, password }); const item = Array.isArray(raw) ? raw[0] : raw; if (!item.success) throw new Error(item.error || 'Login failed'); // item.token is the session token @@ -316,8 +316,8 @@ function buildSchema(base: string, params?: Record { const payload: Record = { schema }; @@ -130,7 +130,7 @@ import { apiRequest } from '../api/client'; export const authService = { login: async (email: string, password: string) => { - const raw = await apiRequest('account_login', { email, password }); + const raw = await apiRequest('Account|login', { email, password }); // Normalize array vs object response const item = Array.isArray(raw) ? raw[0] : raw; if (!item.success) throw new Error(item.error || 'Login failed'); @@ -140,38 +140,38 @@ export const authService = { }, register: async (name: string, email: string, password: string) => { - const res = await apiRequest('account_create', { name, email, password }); + const res = await apiRequest('Account|create', { name, email, password }); if (res?.success && res?.value) localStorage.setItem('session_token', res.value); return res; }, - getProfile: () => apiRequest('account'), + getProfile: () => apiRequest('Account|read'), logout: async () => { - await apiRequest('account_logout').catch(() => {}); + await apiRequest('Account|logout').catch(() => {}); localStorage.removeItem('session_token'); }, - updateProfile: (data: unknown) => apiRequest('account_update', data), + updateProfile: (data: unknown) => apiRequest('Account|update', data), changePassword: (currentPassword: string, newPassword: string) => - apiRequest('account_password', { current_password: currentPassword, new_password: newPassword }), + apiRequest('Account|password', { current_password: currentPassword, new_password: newPassword }), - requestPasswordReset: (email: string) => apiRequest('account_reset', { email }), - verifyAccount: (token: string) => apiRequest('account_verify', { token }), + requestPasswordReset: (email: string) => apiRequest('Account|reset', { email }), + verifyAccount: (token: string) => apiRequest('Account|verify', { token }), // OAuth - getGoogleOAuthUrl: (page: string) => apiRequest(`account_google?page=${page}`), - getGithubOAuthUrl: (page: string) => apiRequest(`account_github?page=${page}`), - exchangeOAuthSession: (sessionid: string) => apiRequest('account_oauth', { sessionid }), - loginWithGoogle: (token: string) => apiRequest('account_login_google', { token }), - loginWithGithub: (token: string) => apiRequest('account_login_github', { token }), + getGoogleOAuthUrl: (page: string) => apiRequest(`Account|google?page=${page}`), + getGithubOAuthUrl: (page: string) => apiRequest(`Account|github?page=${page}`), + exchangeOAuthSession: (sessionid: string) => apiRequest('Account|oauth', { sessionid }), + loginWithGoogle: (token: string) => apiRequest('Account|login_google', { token }), + loginWithGithub: (token: string) => apiRequest('Account|login_github', { token }), // 2FA - generate2FA: () => apiRequest('account_2fa_generate'), - enable2FA: (token: string) => apiRequest('account_2fa_enable', { token }), - disable2FA: () => apiRequest('account_2fa_disable'), - verify2FA: (token: string) => apiRequest('account_2fa_verify', { token }), + generate2FA: () => apiRequest('Account|2fa_generate'), + enable2FA: (token: string) => apiRequest('Account|2fa_enable', { token }), + disable2FA: () => apiRequest('Account|2fa_disable'), + verify2FA: (token: string) => apiRequest('Account|2fa_verify', { token }), }; ``` @@ -317,22 +317,22 @@ function buildSchema(base: string, params?: Record): string { export const postsService = { list: (params?: { page?: number; limit?: number; status?: string }) => - apiRequest(buildSchema('posts_list', params)), + apiRequest(buildSchema('Posts|list', params)), - read: (id: string) => apiRequest(`posts_read/${id}`), + read: (id: string) => apiRequest('Posts|read', { id }), create: (data: Pick) => - apiRequest('posts_create', data), + apiRequest('Posts|create', data), update: (id: string, data: Partial) => - apiRequest(`posts_update/${id}`, data), + apiRequest('Posts|update', { ...data, id }), - remove: (id: string) => apiRequest(`posts_remove/${id}`), + remove: (id: string) => apiRequest('Posts|remove', { id }), - publish: (id: string) => apiRequest(`posts_publish/${id}`), + publish: (id: string) => apiRequest('Posts|publish', { id }), search: (query: string, options?: { limit?: number; mode?: string }) => - apiRequest(buildSchema('posts_search', options), { query }), + apiRequest(buildSchema('Posts|search', options), { query }), }; ``` @@ -468,7 +468,7 @@ async function handleFileSelect(file: File) { const uploadData = await uploadFile(file); // Register in the app via the main API - const res = await apiRequest('documents_create', { + const res = await apiRequest('Documents|create', { id: uploadData.id, url: uploadData.url, name: uploadData.name, diff --git a/frontend-integration/07-react-native-integration.md b/frontend-integration/07-react-native-integration.md index f0cde9b..a72a39f 100644 --- a/frontend-integration/07-react-native-integration.md +++ b/frontend-integration/07-react-native-integration.md @@ -49,7 +49,7 @@ Expo exposes public client variables with the `EXPO_PUBLIC_` prefix. Keep secret | `EXPO_PUBLIC_API_BASE_URL` | Default API host. | | `EXPO_PUBLIC_API_BASE_URL_DEV` | Dev API host override. | | `EXPO_PUBLIC_API_BASE_URL_PRODUCTION` | Production API host override. | -| `EXPO_PUBLIC_API_PATH` | API path, usually `/` for `ROUTE('API / ...')` projects or `/api/` for conventional deployments. | +| `EXPO_PUBLIC_API_PATH` | API path, usually `/` for actions with `route: 'API /'` or `/api/` for conventional deployments. | | `EXPO_PUBLIC_UPLOAD_URL` | File service upload base URL. | | `EXPO_PUBLIC_UPLOAD_TOKEN` | Optional file service token. | | `EXPO_PUBLIC_UPLOAD_AUTH_HEADER` | Optional header name for the upload token, for example `Authorization`. | @@ -129,37 +129,41 @@ On `401`, clear the token only for protected schemas. Public schemas such as log ## Anonymous Schema Allowlist -Total.js route definitions show the available API schemas, but do not treat the `+` or `-` prefix in the route string as a portable auth contract: +Total.js action declarations show the available API schemas, but do not treat route metadata as a portable auth contract: ```javascript -ROUTE('API / +account_login --> Auth/login'); -ROUTE('+API / -account_logout --> Auth/logout'); -ROUTE('+API / +posts_create --> Posts/create'); +NEWACTION('Posts|read', { + input: '*id', + route: '+API /', + action: function($, model) { + // model.id contains the validated record ID + } +}); ``` Confirm public/protected behavior from backend middleware and real responses, then mirror the public schemas in the mobile client: ```typescript const ANONYMOUS_API_SCHEMAS = new Set([ - 'account_create', - 'account_login', - 'account_login_google', - 'account_login_github', - 'account_oauth', - 'account_reset', - 'account_password_reset', - 'account_verify', - 'posts_list', - 'posts_read', - 'categories_list', + 'Account|create', + 'Account|login', + 'Account|login_google', + 'Account|login_github', + 'Account|oauth', + 'Account|reset', + 'Account|password_reset', + 'Account|verify', + 'Posts|list', + 'Posts|read', + 'Categories|list', ]); ``` -Compare only the base schema before `/` or `?`: +Compare only the base schema before `?`: ```typescript function getBaseSchema(schema: string): string { - return schema.split('?')[0].split('/')[0]; + return schema.split('?')[0]; } ``` @@ -205,7 +209,7 @@ App starts -> restore preferred language -> load token from SecureStore -> if no token: clear auth state and show the public shell - -> if token: set token in store and call account + -> if token: set token in store and call Account|read -> hydrate user and any cheap bootstrap counts -> show the authenticated shell ``` @@ -213,17 +217,17 @@ App starts Login/register flow: ```text -account_login +Account|login -> normalize { token, user? } or a plain token string -> save token to SecureStore -> set token/user in store - -> call account in the background + -> call Account|read in the background ``` Logout flow: ```text -account_logout best-effort +Account|logout best-effort -> delete SecureStore token -> clear auth state -> preserve non-sensitive preferences such as language @@ -273,11 +277,11 @@ Do not call schema strings from screens. Keep typed domain APIs thin: ```typescript export const postsApi = { list: (params?: PostsListParams) => - apiRequest('posts_list', undefined, { query: params }).then(extractItems), + apiRequest('Posts|list', undefined, { query: params }).then(extractItems), read: (id: string) => - apiRequest(`posts_read/${encodeURIComponent(id.trim())}`), + apiRequest('Posts|read', { id: id.trim() }), create: (data: Partial) => - apiRequest('posts_create', data), + apiRequest('Posts|create', data), }; ``` diff --git a/frontend-integration/08-flutter-integration.md b/frontend-integration/08-flutter-integration.md index 091eec2..978fc03 100644 --- a/frontend-integration/08-flutter-integration.md +++ b/frontend-integration/08-flutter-integration.md @@ -62,7 +62,7 @@ Flutter exposes build-time config through `--dart-define`. Do not bundle private | `API_BASE_URL` | Default API host. | | `API_BASE_URL_DEV` | Dev API host override. | | `API_BASE_URL_PRODUCTION` | Production API host override. | -| `API_PATH` | Usually `/` for `ROUTE('API / ...')` or `/api/` for conventional deployments. | +| `API_PATH` | Usually `/` for actions with `route: 'API /'` or `/api/` for conventional deployments. | | `UPLOAD_URL` | File service upload base URL. | | `UPLOAD_TOKEN` | Optional scoped upload token. | | `UPLOAD_AUTH_HEADER` | Optional header name for the upload token, for example `Authorization`. | @@ -243,32 +243,36 @@ String buildSchemaWithQuery(String schema, [Map? query]) { return parts.isEmpty ? schema : '$schema?${parts.join('&')}'; } -String getBaseSchema(String schema) => schema.split('?').first.split('/').first; +String getBaseSchema(String schema) => schema.split('?').first; ``` -Total.js route prefixes are useful hints, but do not treat `+` or `-` as a portable mobile auth contract: +Total.js action declarations are useful references, but do not treat route metadata as a portable mobile auth contract: ```javascript -ROUTE('API / +account_login --> Auth/login'); -ROUTE('+API / -account_logout --> Auth/logout'); -ROUTE('+API / +posts_create --> Posts/create'); +NEWACTION('Posts|read', { + input: '*id', + route: '+API /', + action: function($, model) { + // model.id contains the validated record ID + } +}); ``` Confirm public/protected behavior from backend middleware and real responses, then mirror public schemas in the Flutter client: ```dart const anonymousApiSchemas = { - 'account_create', - 'account_login', - 'account_login_google', - 'account_login_github', - 'account_oauth', - 'account_reset', - 'account_password_reset', - 'account_verify', - 'posts_list', - 'posts_read', - 'categories_list', + 'Account|create', + 'Account|login', + 'Account|login_google', + 'Account|login_github', + 'Account|oauth', + 'Account|reset', + 'Account|password_reset', + 'Account|verify', + 'Posts|list', + 'Posts|read', + 'Categories|list', }; bool isAnonymousApiSchema(String schema) => anonymousApiSchemas.contains(getBaseSchema(schema)); @@ -350,7 +354,7 @@ App starts -> restore persisted non-secret preferences -> load token from secure storage -> if no token: clear auth state and show the public shell - -> if token: set token in memory and call account + -> if token: set token in memory and call Account|read -> hydrate user and any cheap bootstrap counts -> show the authenticated shell ``` @@ -358,17 +362,17 @@ App starts Login/register flow: ```text -account_login +Account|login -> normalize { token, user? } or a plain token string -> save token to secure storage -> set token/user in app state - -> call account in the background + -> call Account|read in the background ``` Logout flow: ```text -account_logout best-effort +Account|logout best-effort -> delete secure storage token -> clear auth state -> preserve non-sensitive preferences such as language @@ -379,7 +383,7 @@ Example service: ```dart class AuthService { Future> login(String email, String password) async { - final res = await apiRequest('account_login', data: { + final res = await apiRequest('Account|login', data: { 'email': email, 'password': password, }); @@ -392,12 +396,12 @@ class AuthService { } Future> account() async { - return apiRequest>('account'); + return apiRequest>('Account|read'); } Future logout() async { try { - await apiRequest('account_logout'); + await apiRequest('Account|logout'); } catch (_) { // Logout should still clear local state when the backend is unreachable. } @@ -457,7 +461,7 @@ class PostsApi { String? search, }) async { final payload = await apiRequest( - 'posts_list', + 'Posts|list', query: {'page': page, 'search': search}, ); return extractItems>(payload); @@ -465,12 +469,13 @@ class PostsApi { Future> read(String id) { return apiRequest>( - 'posts_read/${Uri.encodeComponent(id.trim())}', + 'Posts|read', + data: {'id': id.trim()}, ); } Future> create(Map data) { - return apiRequest>('posts_create', data: data); + return apiRequest>('Posts|create', data: data); } } ``` diff --git a/frontend-integration/README.md b/frontend-integration/README.md index b125df0..79c66af 100644 --- a/frontend-integration/README.md +++ b/frontend-integration/README.md @@ -26,8 +26,8 @@ This guide stack is designed to be reused across projects. Replace `totaljsbacke ## The three facts you need to know first ``` -1. Every API call is: POST https://totaljsbackend.com/api/ (or / when the backend uses ROUTE('API / ...')) -2. Every request body: { "schema": "resource_action[/id][?params]", "data": { ... } } +1. Every API call is: POST https://totaljsbackend.com/api/ (or the path declared by NEWACTION route) +2. Every request body: { "schema": "Namespace|action[?params]", "data": { ... } } 3. Every auth: x-token: (header, injected globally) ``` diff --git a/readme.md b/readme.md index b695071..88127f3 100644 --- a/readme.md +++ b/readme.md @@ -42,6 +42,32 @@ Before adding an import, wrapper, service layer, repository, dependency injectio If the answer is yes, use the Total.js mechanism. Internal `require()` calls and external abstractions should be rare, intentional, and easy to justify. +## Action Convention + +For new Total.js 5 APIs, use stable action IDs in the form `Namespace|action`. Put record identifiers in the validated input instead of encoding them in the action name: + +```javascript +NEWACTION('Posts|read', { + input: '*id', + route: '+API /api/', + action: async function($, model) { + var post = await DATA.read('tbl_post').id(model.id).error(404).promise($); + $.callback(post); + } +}); +``` + +The client calls the action with: + +```json +{ + "schema": "Posts|read", + "data": { "id": "post-id" } +} +``` + +Do not introduce legacy-style public names such as `posts_something` or `posts_read/{id}` in new code. + ## How To Use This Repo For a new Total.js project, copy or reference this repository as AI context before asking an agent to implement backend work. Point the agent to [AGENTS.md](AGENTS.md), then to the guide that matches the feature. diff --git a/totaljs/actions.md b/totaljs/actions.md index 29acd6a..f7ed9c7 100644 --- a/totaljs/actions.md +++ b/totaljs/actions.md @@ -2,8 +2,8 @@ Total.js 5 has two first-class ways to declare actions. Both run through the same `$` context and `ACTION()` caller. -- **`NEWSCHEMA('Name', ...)` + `schema.action()`** — group a domain. Preferred inside `plugins//schemas/`. -- **`NEWACTION('Name|action', options)`** — one action, optionally with its own `route`. Preferred in `/actions/` for isolated operations. +- **`NEWSCHEMA('Name', ...)` + `schema.action()`** — group internal or existing schema actions. +- **`NEWACTION('Name|action', options)`** — declare a stable public action ID together with its input and optional route. Prefer this form for new API contracts. Do not add service classes underneath them. @@ -95,7 +95,7 @@ The schema must be defined in the form: `name:String, age:Number` separated by t ### Basic Query and Params Example ```javascript -NEWACTION('Find', { +NEWACTION('Examples|find', { query: 'page:Number, sort:String', params: 'projectid:String', route: '+API ?', @@ -122,7 +122,7 @@ NEWACTION('Find', { ### Input/Output Schema Example ```javascript -NEWACTION('Save', { +NEWACTION('Examples|save', { input: '*name:String, age:Number', output: 'success:Boolean', params: 'projectid:String, id:String', @@ -177,33 +177,49 @@ NEWSCHEMA('Orders', function(schema) { }); ``` -Register routes in the plugin: +For a new public API, declare the routed contract directly with `NEWACTION` so its public name, validation, and route stay together: ```javascript -exports.install = function() { - ROUTE('+API /api/ -orders_list --> Orders/list'); - ROUTE('+API /api/ +orders_create --> Orders/create'); -}; +NEWACTION('Orders|list', { + query: 'page:Number,limit:Number,search:String', + route: '+API /api/', + action: async function($) { + var response = await DATA.list('tbl_order') + .where('isremoved', false) + .autoquery($.query, 'id:String,name:String,dtcreated:Date', 'dtcreated_desc', 100) + .promise($); + $.callback(response); + } +}); + +NEWACTION('Orders|create', { + input: '*name:String', + route: '+API /api/', + action: async function($, model) { + model.id = UID(); + await DATA.insert('tbl_order', model).promise($); + $.success(model.id); + } +}); ``` -`+API` / `-API` is authorization (see [auth.md](auth.md)). The `+` / `-` immediately before the schema name (`-orders_list`, `+orders_create`) is the Total.js API convention for read vs write. In Total.js 5 that prefix is stripped from the public schema name; clients call `orders_list`, not `-orders_list`. +The leading `+API` requires an authenticated session (see [auth.md](auth.md)). The framework exposes the action ID unchanged, so clients call `Orders|list` or `Orders|create` and send declared inputs in `data`. Call any action from code: ```javascript -await ACTION('Orders/create', { name: 'X' }).promise($); +await ACTION('Orders/create', { name: 'X' }).promise($); // grouped NEWSCHEMA action await ACTION('Users|insert', {}).promise($); ``` ## Routing ```javascript -ROUTE('+API /api/ --> action1'); ROUTE('+GET /api/products/ --> action1 action2 action3'); ROUTE('+POST /api/products/add/ --> action1 action2 (response) action3'); ``` -We recommend using `NEWSCHEMA` plus plugin `ROUTE()` for feature APIs, or `NEWACTION` with `options.route` for isolated actions. What is the `API` HTTP method? It's similar to the `POST` HTTP method, but it has an exact JSON structure, for example: `{ "schema": "action_name", "data": Object }`. +Use ordinary `ROUTE()` declarations for HTTP endpoints that are not action-based JSON contracts. For a new API Routing contract, use `NEWACTION('Namespace|action', { route, input, action })`. The `API` method uses a POST request with the exact JSON envelope `{ "schema": "Namespace|action", "data": Object }`. ## Examples @@ -389,4 +405,4 @@ NEWACTION('Todo|clear', { $.success(); } }); -``` \ No newline at end of file +``` diff --git a/totaljs/architecture.md b/totaljs/architecture.md index c365d4f..a47ee75 100644 --- a/totaljs/architecture.md +++ b/totaljs/architecture.md @@ -122,7 +122,7 @@ CLI scripts, one-off migrations, and tests that run *outside* `Total.run()` may | Auth | `AUTH()` in `definitions/auth.js` | | Feature HTTP API | `plugins//index.js` + `schemas/` | | Isolated action | `NEWACTION()` in `/actions/` or a plugin schema | -| Upload, health, webhook, OAuth redirect | `controllers/` + `ROUTE('GET|POST|FILE ...')` | +| Upload, health, webhook, OAuth redirect | `controllers/` + `ROUTE('GET\|POST\|FILE ...')` | | Periodic work | `CRON()` or `ON('service')` in definitions | | Heavy isolated process | `/workers/*.js` | | Config value | `CONF.key` from `config` | @@ -138,43 +138,44 @@ A backend feature is a plugin: exports.name = '@(Orders)'; exports.icon = 'ti ti-receipt'; exports.position = 10; - -exports.install = function() { - ROUTE('+API /api/ -orders_list --> Orders/list'); - ROUTE('+API /api/ -orders_read/{id} --> Orders/read'); - ROUTE('+API /api/ +orders_create --> Orders/create'); -}; ``` ```javascript -// plugins/orders/schemas/orders.js -NEWSCHEMA('Orders', function(schema) { - - schema.action('list', { - query: 'page:Number,limit:Number,search:String', - action: async function($) { - if (!FUNC.require_perm($, 'orders.read')) - return; - var p = FUNC.paginate($.query); - var result = await DATA.list('tbl_order') - .where('isremoved', false) - .autoquery($.query, 'id:String,name:String,dtcreated:Date', 'dtcreated_desc', 100) - .paginate(p.page, p.limit) - .promise($); - $.callback(FUNC.list_payload(result, p.page, p.limit)); - } - }); +// plugins/orders/index.js (continued) +NEWACTION('Orders|list', { + query: 'page:Number,limit:Number,search:String', + permissions: 'orders.read', + route: '+API /api/', + action: async function($) { + var p = FUNC.paginate($.query); + var result = await DATA.list('tbl_order') + .where('isremoved', false) + .autoquery($.query, 'id:String,name:String,dtcreated:Date', 'dtcreated_desc', 100) + .paginate(p.page, p.limit) + .promise($); + $.callback(FUNC.list_payload(result, p.page, p.limit)); + } +}); + +NEWACTION('Orders|read', { + input: '*id:UID', + permissions: 'orders.read', + route: '+API /api/', + action: async function($, model) { + var item = await DATA.read('tbl_order').id(model.id).error(404).promise($); + $.callback(item); + } }); ``` -The schema file is auto-loaded. Do not `require()` it from the plugin. +The plugin entry file is loaded with the feature. As the feature grows, split supporting schema code into the plugin's auto-loaded `schemas/` folder; do not move its SQL into a controller. ## `NEWACTION` vs `NEWSCHEMA` Both are first-class Total.js 5 APIs. -- **`NEWSCHEMA('Name', ...)` + `schema.action()`** — group related actions. Preferred for plugin domains. -- **`NEWACTION('Name|action', { route, action })`** — standalone action, can declare its own route. +- **`NEWSCHEMA('Name', ...)` + `schema.action()`** — group internal or existing schema actions. +- **`NEWACTION('Name|action', { route, input, action })`** — preferred for new public API contracts because the stable ID, validation, and route are declared together. Do not add a third style (service class that both call). Do not put business rules in controllers when an action exists. @@ -187,7 +188,7 @@ POST /api/ Content-Type: application/json x-token: -{ "schema": "orders_list?page=1", "data": {} } +{ "schema": "Orders|list?page=1", "data": {} } ``` Use ordinary HTTP routes only for files, health, webhooks, SSO redirects, and WebSockets. diff --git a/totaljs/auth.md b/totaljs/auth.md index 57949ce..e4ea4c7 100644 --- a/totaljs/auth.md +++ b/totaljs/auth.md @@ -92,11 +92,12 @@ FUNC.create_session = async function($, userid) { }; ``` -Login action: +Login action and its public API route: ```javascript -schema.action('login', { +NEWACTION('Account|login', { input: '*email:Email,*password:String', + route: '-API /api/', action: async function($, model) { var user = await DATA.read('tbl_user') .where('email', model.email.toLowerCase().trim()) @@ -120,12 +121,23 @@ schema.action('login', { ## Route flags ```javascript -ROUTE('-API /api/ +auth_login --> Auth/login'); // public -ROUTE('+API /api/ -auth_me --> Auth/me'); // session required -ROUTE('+API /api/ +auth_logout --> Auth/logout'); +NEWACTION('Account|read', { + route: '+API /api/', + action: function($) { + $.callback(FUNC.user_safe($.user)); + } +}); + +NEWACTION('Account|logout', { + route: '+API /api/', + action: function($) { + // Remove the current session from MAIN and DATA here. + $.success(); + } +}); ``` -Public login/register stay on `-API`. Protected work stays on `+API`. +Public login/register actions use `route: '-API /api/'`. Protected work uses `route: '+API /api/'`. Optional personalization (public list that can use a user if present) uses an unprefixed `API` route and reads `$.user` if `AUTH()` succeeded. diff --git a/totaljs/controllers-and-routing.md b/totaljs/controllers-and-routing.md index e001e78..5b008be 100644 --- a/totaljs/controllers-and-routing.md +++ b/totaljs/controllers-and-routing.md @@ -104,11 +104,10 @@ ROUTE('POST /hooks/stripe', handler); ROUTE('+POST /upload/', handler, ['upload'], 1024 * 10); ROUTE('FILE /documents/*.*', handler); ROUTE('SOCKET /realtime/', handler); -ROUTE('API /api/ -ping --> Api/ping'); -ROUTE('+API /api/ +orders_create --> Orders/create'); -ROUTE('-API /api/ +auth_login --> Auth/login'); ``` +Declare JSON API actions with `NEWACTION()` so the action ID, validation, and API route remain in one place. + ### Auth flags The first character of the method is an auth flag: @@ -126,28 +125,45 @@ This is evaluated from `AUTH()`. See [auth.md](auth.md). `API` is POST plus a JSON envelope `{ schema, data }`. ```javascript -ROUTE('+API /api/ -orders_list --> Orders/list'); -ROUTE('+API /api/ -orders_read/{id} --> Orders/read'); -ROUTE('+API /api/ +orders_create --> Orders/create'); +NEWACTION('Orders|list', { + route: '+API /api/', + action: async function($) { + $.callback(await DATA.list('tbl_order').promise($)); + } +}); + +NEWACTION('Orders|read', { + input: '*id:UID', + route: '+API /api/', + action: async function($, model) { + $.callback(await DATA.read('tbl_order').id(model.id).error(404).promise($)); + } +}); + +NEWACTION('Orders|create', { + input: '*name:String', + route: '+API /api/', + action: async function($, model) { + model.id = UID(); + await DATA.insert('tbl_order', model).promise($); + $.success(model.id); + } +}); ``` -The first flag after the path (`-` / `+` before the schema name) is the Total.js API operation type (read vs write), not the same thing as HTTP auth. Auth is the `+API` / `-API` prefix. +The `+API` prefix requires authentication. The presence of `input` controls validation; it does not change the public action ID. Client call: ```http POST /api/ x-token: ... -{ "schema": "orders_read/abc123", "data": {} } +{ "schema": "Orders|read", "data": { "id": "abc123" } } ``` ### Action composition -```javascript -ROUTE('+API /api/ +orders_create --> Orders/check Orders/insert (response)'); -``` - -Use this for reusable preconditions (uniqueness, ownership). Do not build hidden pipelines with side effects. +Call reusable preconditions explicitly from the public action with `ACTION('Orders|check', model)`, then return the final result. This keeps composition visible and avoids route strings that hide a pipeline of side effects. ### `NEWACTION` routes diff --git a/totaljs/mobile-backend.md b/totaljs/mobile-backend.md index fdd74ea..5d7bce3 100644 --- a/totaljs/mobile-backend.md +++ b/totaljs/mobile-backend.md @@ -26,7 +26,7 @@ Content-Type: application/json x-token: { - "schema": "products_list?page=1&limit=20", + "schema": "Products|list?page=1&limit=20", "data": { "optional": "payload" } } ``` @@ -35,7 +35,7 @@ Some apps mount API Routing at `/` instead of `/api/`. The client must configure Guarantees: -- schema names are stable and action-oriented +- public action IDs are stable and use `Namespace|action` - public vs protected is explicit (`-API` / `+API`) - login/register can return `{ token, user }` - lists are arrays or `{ items, count, page, limit }` diff --git a/totaljs/mobile-backend/01-architecture.md b/totaljs/mobile-backend/01-architecture.md index adfcc81..3a4d904 100644 --- a/totaljs/mobile-backend/01-architecture.md +++ b/totaljs/mobile-backend/01-architecture.md @@ -43,7 +43,7 @@ Do not put feature rules in the mobile app. If a user may publish only what they - CommonJS files, Total.js globals, no internal `require()` - tabs and semicolons - PascalCase schema names: `NEWSCHEMA('Orders', ...)` -- snake_case public schemas: `orders_list` +- stable public action IDs: `Namespace|action` (for example `Orders|list`) ```javascript // WRONG diff --git a/totaljs/mobile-backend/02-api-routing.md b/totaljs/mobile-backend/02-api-routing.md index ffd5dae..050883c 100644 --- a/totaljs/mobile-backend/02-api-routing.md +++ b/totaljs/mobile-backend/02-api-routing.md @@ -10,8 +10,8 @@ Content-Type: application/json x-token: { - "schema": "orders_read/ord123?include=items", - "data": { "optional": "payload" } + "schema": "Orders|read?include=items", + "data": { "id": "ord123" } } ``` @@ -20,20 +20,17 @@ The path is project-defined (`/api/` or `/`). Configure it in the client. ## Schema string ```text - -/ -// -?key=value -/?key=value +| +|?key=value ``` Examples: ```text -auth_login -orders_list?page=1&limit=20 -orders_read/ord123 -orders_update/ord123 +Account|login +Orders|list?page=1&limit=20 +Orders|read +Orders|update ``` ## Naming @@ -42,21 +39,22 @@ Readable, action-oriented, stable: | Schema | Meaning | |--------|---------| -| `auth_login` | login | -| `auth_me` | current user | -| `orders_list` | list | -| `orders_read/{id}` | detail | -| `orders_create` | create | +| `Account\|login` | login | +| `Account\|read` | current user | +| `Orders\|list` | list | +| `Orders\|read` | detail; `id` is declared input | +| `Orders\|create` | create | Do not leak table names (`tbl_order_select`). Do not rename schemas casually — they are as public as REST URLs. ## Query and params -Filters belong in the schema string, not on the HTTP URL. +Filters belong in the schema string, not on the HTTP URL. Record IDs and other action inputs belong in `data`. ```javascript -schema.action('list', { +NEWACTION('Orders|list', { query: 'search:String,limit:Number,page:Number,sort:String', + route: '+API /api/', action: async function($) { var response = await DATA.list('view_order') .where('isremoved', false) @@ -68,12 +66,11 @@ schema.action('list', { ``` ```javascript -ROUTE('+API /api/ -orders_read/{id} --> Orders/read'); - -schema.action('read', { - params: '*id:UID', - action: async function($) { - var item = await DATA.read('view_order').id($.params.id).error(404).promise($); +NEWACTION('Orders|read', { + input: '*id:UID', + route: '+API /api/', + action: async function($, model) { + var item = await DATA.read('view_order').id(model.id).error(404).promise($); $.success(item); } }); @@ -91,4 +88,4 @@ The client should normalize HTTP errors, `{ success: false, ... }`, validation e ## Versioning -When a contract changes, add `orders_list_v2` (or a clearly new name) and keep the old schema until old app builds die. Compatibility aliases (`logo` and `logoUrl`) are allowed if documented. +When a contract changes, add `Orders|list_v2` (or a clearly new action name) and keep the old action until old app builds die. Compatibility aliases (`logo` and `logoUrl`) are allowed if documented. diff --git a/totaljs/mobile-backend/03-plugins-routes.md b/totaljs/mobile-backend/03-plugins-routes.md index 1b673f8..0e4ee71 100644 --- a/totaljs/mobile-backend/03-plugins-routes.md +++ b/totaljs/mobile-backend/03-plugins-routes.md @@ -1,6 +1,6 @@ # Plugins And Route Registration -A plugin is the feature unit. Routes live in `exports.install()`. Actions live in auto-loaded `schemas/`. +A plugin is the feature unit. Public JSON contracts use `NEWACTION()` so each route and its validation live with the action ID. Keep ordinary HTTP, file, and socket routes in `exports.install()`. ```javascript exports.icon = 'ti ti-box'; @@ -10,43 +10,70 @@ exports.visible = function(user) { return user.sa || (user.permissions || []).includes('orders'); }; -exports.install = function() { - ROUTE('+API /api/ -orders_list --> Orders/list'); - ROUTE('+API /api/ -orders_read/{id} --> Orders/read'); - ROUTE('+API /api/ +orders_create --> Orders/create'); -}; +NEWACTION('Orders|list', { + route: '+API /api/', + action: async function($) { + $.callback(await DATA.list('tbl_order').promise($)); + } +}); + +NEWACTION('Orders|read', { + input: '*id:UID', + route: '+API /api/', + action: async function($, model) { + $.callback(await DATA.read('tbl_order').id(model.id).error(404).promise($)); + } +}); + +NEWACTION('Orders|create', { + input: '*name:String', + route: '+API /api/', + action: async function($, model) { + model.id = UID(); + await DATA.insert('tbl_order', model).promise($); + $.success(model.id); + } +}); ``` Do not scatter feature routes through controllers. Controllers keep gateway-level HTTP (upload, health, sockets). -## Auth vs operation prefix +## Auth route prefix ```javascript -ROUTE('+API /api/ -orders_list --> Orders/list'); -ROUTE('-API /api/ +auth_login --> Auth/login'); +NEWACTION('Orders|list', { + route: '+API /api/', + action: function($) { + // Return the authorized user's order list. + } +}); + +NEWACTION('Account|login', { + input: '*email:Email,*password:String', + route: '-API /api/', + action: function($, model) { + // Validate credentials and return the session. + } +}); ``` - `+API` / `-API` — session required / public -- `-orders_list` / `+orders_create` — Total.js read/write convention; the public name is without the prefix +- `input` — validated client data; it does not alter the stable public action ID ## Composition -```javascript -ROUTE('+API /api/ +orders_create --> Orders/check Orders/insert (response)'); -``` - -Use for reusable preconditions (uniqueness, ownership). Do not hide a workflow behind six silent actions. +Call reusable preconditions explicitly with `ACTION('Orders|check', model)` inside `Orders|create`. Do not hide a workflow behind a route string containing six silent actions. ## Public allowlist Publish the public schema names. The mobile app should not guess from `+`/`-`. ```text -auth_login -auth_register -api_ping -catalog_list -catalog_read +Account|login +Account|create +API|ping +Catalog|list +Catalog|read ``` Protected schemas return 401 without a token. Public schemas must not log the user out if a stale token is sent — either use `-API` or ignore a bad token on those operations. @@ -68,7 +95,12 @@ Keep these rare. Most mobile work stays in API Routing. exports.install = function() { if (!FUNC.pack_enabled('inventory')) return; - ROUTE('+API /api/ -inventory_list --> Inventory/list'); + NEWACTION('Inventory|list', { + route: '+API /api/', + action: async function($) { + $.callback(await DATA.list('tbl_inventory').promise($)); + } + }); }; ``` diff --git a/totaljs/plugin.md b/totaljs/plugin.md index e1977d6..40f9757 100644 --- a/totaljs/plugin.md +++ b/totaljs/plugin.md @@ -52,10 +52,21 @@ exports.permissions = [{ id: 'myitems_view', name: 'View My Items' }, { id: 'myi // Optional permission catalog for a Total.js admin/UI client. exports.install = function() { - ROUTE('+API /api/ -todo_list --> Todo/list'); - ROUTE('+API /api/ +todo_create --> Todo/create'); - // FILE / SOCKET / upload routes also belong here when they are this feature's + // Optional plugin lifecycle setup. Ordinary FILE, SOCKET, or upload routes + // can be registered here when they belong exclusively to this feature. }; + +// Declare JSON API contracts with NEWACTION so the route and validation stay +// next to the public action ID. +NEWACTION('Todo|create', { + input: '*name:String', + route: '+API /api/', + action: async function($, model) { + model.id = UID(); + await DATA.insert('tbl_todo', model).promise($); + $.success(model.id); + } +}); ``` Call `CORS()` once for the app, not in every plugin. @@ -68,7 +79,7 @@ __Syntax__: - **`exports.visible`** `Function(user_session)`: A function that returns `true` or `false` to control main visibility based on user properties (e.g., `user.sa` for super admin, `user.permissions`). - **`exports.permissions`** `Object Array`: (Optional) An array of objects defining new permissions that this plugin introduces. Each object should have an `id` and a `name`. - **`exports.hidden`** `Boolean`: Hides the plugin in the navigation on the client-side. -- **`exports.install`** `Function()`: This function is crucial for setting up special routes using the `ROUTE()`. +- **`exports.install`** `Function()`: Optional plugin lifecycle setup for special HTTP, file, or socket routes and other feature initialization. ## Plugin example @@ -115,6 +126,11 @@ Conditional packs (optional product modules) can skip route registration: exports.install = function() { if (!FUNC.feature_enabled('todo')) return; - ROUTE('+API /api/ -todo_list --> Todo/list'); + NEWACTION('Todo|list', { + route: '+API /api/', + action: async function($) { + $.callback(await DATA.list('tbl_todo').promise($)); + } + }); }; -``` \ No newline at end of file +``` diff --git a/totaljs/style.md b/totaljs/style.md index 55623f9..1259a45 100644 --- a/totaljs/style.md +++ b/totaljs/style.md @@ -40,7 +40,7 @@ SQL and JS should use the same names. See [databases.md](databases.md). - Lowercase filenames - Kebab-case only when a module name is several words: `ai-engine.js` - Plugin schema names are PascalCase: `NEWSCHEMA('Orders', ...)` -- Public schema strings are snake_case: `orders_list` +- Public action IDs use `Namespace|action`: `Orders|list` ## Comments