Taskboard
Total.js referenceA small workspace with private tasks and a shared administrator view.
No tasks yet.
# Total.js Forge - full cookbook ## Get started with Total.js 5 Source: https://forge.totaljs.es/en/docs/getting-started/ Framework: total5@0.0.19 Content-SHA256: 5f7bf99c3dc733b1a14f49c26a40d678238c8c9e2e60d732ca14763a7eab43e7 Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c Sources: total5-installed@total5@0.0.19 Review: not-recorded Total.js 5 is the npm package `total5`. You need Node.js (the examples of this site run on Node.js 24) and an empty folder. Create the project and install the package: ```bash mkdir hello && cd hello npm init -y npm install total5 ``` The start script is a file named `index.js` in the root of the project. It loads the framework and starts the server: ```js require('total5'); Total.run({ port: 8000 }); ``` That is all the code you write to start. The files in the folders of the project (see the project structure page) are loaded automatically. Create these three files. A **controller** declares the routes. `ROUTE('GET /')` with no handler would show the view with the same name; here the handler renders `views/index.html` with a model: ```js controllers/index.js exports.install = function() { ROUTE('GET /', home); ROUTE('POST /greet/ --> *Greet/say'); }; function home($) { $.view('index', { title: 'Hello Total.js' }); } ``` An **action** has its own validation. The route above runs `Greet/say` with the JSON body of the request, and the framework rejects a body that does not match `input`: ```js definitions/greet.js NEWACTION('Greet/say', { name: 'Say hello', input: '*name:String', action: function($, model) { $.callback({ message: 'Hello, ' + model.name + '!' }); } }); ``` A **view** is HTML with `@{...}` blocks. `views/layout.html` wraps every view and `@{body}` is the place of the view: ```html views/layout.html
A small workspace with private tasks and a shared administrator view.
No tasks yet.
Private tasks with attachments, updated live for the people who may see them.
No tasks yet.
Private tasks stored in SQLite, with an atomic archive of the completed ones.
No tasks yet.
` block (pass the body as a text). | [logmail](/en/total5/logmail/) |
| `Mail` | object | The class behind the email sending (SMTP). | [send-email](/en/total5/send-email/) |
| `AIMODEL` | function | Starts a request for a language model registered with `NEWAIMODEL`: `AIMODEL('name').user('text').callback($.callback())`. | [ai-models](/en/total5/ai-models/) |
| `AIPARSER` | function | Creates a parser for the streamed answer of an AI provider (`openai`, `claude`...): `write()` takes pieces and `end()` returns the content, thinking and tool calls. | [ai-models](/en/total5/ai-models/) |
| `NEWAIMODEL` | function | Registers the driver `function(ai, done)` of one or more AI models for `AIMODEL`, with an optional configuration. | [ai-models](/en/total5/ai-models/) |
| `PYPELINE` | function | Runs a Python file of the `pypelines/` folder (or inline Python code) from the application and talks to it with JSON frames over a socket. | [pypeline](/en/total5/pypeline/) |
## Files, system and workers
| Name | Kind | What it does | Recipe |
|---|---|---|---|
| `PATH` | object | Helpers that build paths in the project folders (`PATH.root()`, `PATH.public()`, `PATH.tmp()`...). | [file-download](/en/total5/file-download/) |
| `MERGE` | function | Registers a virtual file made of several files, minified. | [minification](/en/total5/minification/) |
| `TOUCH` | function | Clears the cached minified, temporary and not-found entries of a URL, or of all of them. | [touch-static-cache](/en/total5/touch-static-cache/) |
| `CLEANUP` | function | Destroys a stream when it has finished. | [cleanup-streams](/en/total5/cleanup-streams/) |
| `BACKUP` | function | Packs a directory into one file. | [backup-restore-shell](/en/total5/backup-restore-shell/) |
| `RESTORE` | function | Unpacks a file created with `BACKUP`. | [backup-restore-shell](/en/total5/backup-restore-shell/) |
| `SHELL` | function | Runs a shell command in the folder of the project. | [backup-restore-shell](/en/total5/backup-restore-shell/) |
| `NPMINSTALL` | function | Installs an npm package in the modules folder of the application. | [load-and-npminstall](/en/total5/load-and-npminstall/) |
| `IMPORT` | function | Downloads a JavaScript file from a URL and loads it. | [import-remote-module](/en/total5/import-remote-module/) |
| `Image` | object | Image processing with GraphicsMagick or ImageMagick. | [images](/en/total5/images/) |
| `NEWTHREAD` | function | Starts a worker thread. | [workers-and-threads](/en/total5/workers-and-threads/) |
| `NEWFORK` | function | Starts a child process. | [workers-and-threads](/en/total5/workers-and-threads/) |
| `NEWTHREADPOOL` | function | Creates a pool of workers that jobs borrow one at a time. | [thread-pool](/en/total5/thread-pool/) |
| `CRON` | function | Runs a function on a schedule. | [cron-jobs](/en/total5/cron-jobs/) |
| `setTimeout2` | function | `setTimeout` with an id: calling it again with the same id replaces the previous timer. | [debounce-timers](/en/total5/debounce-timers/) |
| `clearTimeout2` | function | Cancels a timer created with `setTimeout2`. | [debounce-timers](/en/total5/debounce-timers/) |
| `Flow` | object | The manager of the FlowStreams of the application: `Flow.load(schema)`, `Flow.remove(id)`, `Flow.instances` and `Flow.test()` for one component. | [flowstream](/en/total5/flowstream/), [flowstream-component-test](/en/total5/flowstream-component-test/) |
| `FlowStream` | object | The same object as `Flow`. | [flowstream](/en/total5/flowstream/) |
| `NEWMACRO` | function | Compiles a macro from a string. | [macros](/en/total5/macros/) |
| `NEWCOMPONENT` | function | Compiles the HTML of a component. | [native-components](/en/total5/native-components/) |
| `COMPONENTATOR` | function | Downloads a Componentator bundle into `public/` as `ui--.min.js` and stores its address in `F.repo[name]`. | [remote-service-boundaries](/en/total5/remote-service-boundaries/) |
## Utilities, templates and localization
| Name | Kind | What it does | Recipe |
|---|---|---|---|
| `U` | object | The utility object of the framework (`U.extend()`, `U.clone()`...). | [utilities](/en/total5/utilities/) |
| `Utils` | object | The same object as `U`. | [utilities](/en/total5/utilities/) |
| `UID` | function | Creates a short unique id sorted by time. | [unique-ids-and-hashes](/en/total5/unique-ids-and-hashes/) |
| `GUID` | function | Creates a random string of the given length. | [unique-ids-and-hashes](/en/total5/unique-ids-and-hashes/) |
| `HASH` | function | Returns a fast numeric hash (not for passwords). | [unique-ids-and-hashes](/en/total5/unique-ids-and-hashes/) |
| `CLONE` | function | Makes a deep copy of an object. | [utility-globals](/en/total5/utility-globals/) |
| `COPY` | function | Copies the properties of the first object into the second one and returns the second. | [utility-globals](/en/total5/utility-globals/) |
| `DIFFARR` | function | Compares two arrays of objects by a property and returns the objects to add, the pairs to update and the values to remove. | [diffarr-sync-lists](/en/total5/diffarr-sync-lists/) |
| `QUERIFY` | function | Builds a URL with a query string from an object. | [utility-globals](/en/total5/utility-globals/) |
| `PROMISIFY` | function | `PROMISIFY(fn, a, b, c)` calls a function that takes a callback as the last argument and returns a promise. | [utility-globals](/en/total5/utility-globals/) |
| `ENCRYPT` | function | Turns a value into an opaque token. | [encrypt-and-decrypt](/en/total5/encrypt-and-decrypt/) |
| `DECRYPT` | function | Reads a value from a token created with `ENCRYPT`. | [encrypt-and-decrypt](/en/total5/encrypt-and-decrypt/) |
| `ENCRYPTREQ` | function | Encrypts a value together with the user agent of a request (and its IP address when strict). | [request-bound-tokens](/en/total5/request-bound-tokens/) |
| `DECRYPTREQ` | function | Reads a value created with `ENCRYPTREQ`; returns nothing when the request does not match. | [request-bound-tokens](/en/total5/request-bound-tokens/) |
| `LOCALIZE` | function | Chooses the language of every request. | [localization](/en/total5/localization/) |
| `TRANSLATE` | function | Translates the `@(...)` texts of a string into a language. | [localization](/en/total5/localization/) |
| `Tangular` | object | The template engine (`Tangular.render()`). | [tangular-templates](/en/total5/tangular-templates/) |
| `Ta` | object | Another name of `Tangular`, the template engine. | [template-files](/en/total5/template-files/) |
| `Thelpers` | object | The helper functions available in Tangular templates. | [tangular-templates](/en/total5/tangular-templates/) |
| `TEMPLATE` | function | Renders a Tangular template that is a text, a file of the `templates` folder or a URL, and returns a promise with the text. | [template-files](/en/total5/template-files/) |
```js controllers/globals.js
exports.install = function() {
ROUTE('GET /globals/', globals);
};
function globals($) {
var output = {};
for (var name of Object.getOwnPropertyNames(globalThis)) {
var value;
try {
value = globalThis[name];
} catch (e) {
value = undefined;
}
output[name] = typeof(value) === 'function' ? 'function' : value === null ? 'null' : typeof(value);
}
$.json(output);
}
```
## Configuration keys of Total.js 5
Source: https://forge.totaljs.es/en/docs/configuration-keys/
Framework: total5@0.0.19
Content-SHA256: 2ef72a7b9fcde672c1a24fc856022e87b39a3e5facf68c57efc087c9fb4b90ef
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
The `config` file holds one `key : value` setting per line and the values are available in the global `CONF` object (see the configuration recipe for the file format). The keys below are defined by `total5` 0.0.19 with these defaults; a key that is not here and not in the second table is yours. **Keys that start with `$` are settings of the framework** and `secret_` keys hold secrets.
Set application values in the config file and read them through CONF. The table gives the defaults and the purpose of each setting.
| Key | Default | Note |
|---|---|---|
| `name` | `'Total.js'` | The name of the application. In a view, `@{'%name'}` prints it. |
| `version` | `'1.0.0'` | Application version used in startup output, statistics and the default HTTP ETag suffix (index.js). |
| `author` | `''` | Application author displayed by startup imprint when nonempty (index.js). |
| `secret` | `F.syshash` | Set the application secret explicitly in configuration. |
| `secret_encryption` | `''` | Key read by encrypted WebSocket routes; this is distinct from an action encrypt metadata option (websocket.js). |
| `secret_totalapi` | `''` | Fallback token sent by the built-in TotalAPI driver (api.js); keep server-side. |
| `secret_csrf` | `''` | The secret of the CSRF tokens. Empty: the check always passes. |
| `secret_tms` | `''` | The token that TMS clients must send in the header `x-token`. |
| `$root` | `''` | URL prefix applied to route and asset paths; also adjusts the default API endpoint (routing.js, index.js). |
| `$cors` | `''` | Hostnames separated by comma. |
| `$api` | `'/api/'` | A default API endpoint. |
| `$sourcemap` | `true` | Loader metadata setting; LOAD sets it from the final DEBUG mode or disables it for selective resource loads (index.js). |
| `$httpreqlimit` | `0` | Request limit per ip. |
| `$httpcompress` | `true` | Enables negotiated gzip for eligible responses; the dynamic response path checks a 256-byte threshold (controller.js). |
| `$httpetag` | `''` | Suffix for framework-generated ETags; when empty, configuration derives it from the app version (index.js, controller.js). |
| `$httpmaxsize` | `256` | KB. |
| `$httprangebuffer` | `5120` | 5 MB. |
| `$httptimeout` | `5` | 5 seconds. |
| `$httpfiles` | `an object` | The file extensions that are served as static files, as an object `{ extension: true }`. |
| `$httpchecktypes` | `true` | For multipart data only. |
| `$httpmaxage` | `60` | In seconds. |
| `$httpmaxkeys` | `33` | Maximum parsed query-string key count (utils.js); not a JSON-body field limit. |
| `$httpmaxkey` | `25` | Maximum query-string key length used by the query parser (utils.js). |
| `$httpcacheoffset` | `''` | E.g. 1 minute. |
| `$blacklist` | `''` | HTTP/WebSocket request handling tests whether the configured value contains the client IP (http.js, websocket.js). Not a CIDR parser. |
| `$xpoweredby` | `'Total.js'` | Value of the generated X-Powered-By response header; empty suppresses it in the inspected response paths (controller.js). |
| `$minifyjs` | `true` | Minify the JavaScript files that are served. |
| `$minifycss` | `true` | Minify the CSS files that are served. |
| `$minifyhtml` | `true` | Minify the HTML that is sent. |
| `$port` | `'auto'` | HTTP listen port; auto is resolved at startup, and Total.run options can override it (index.js). |
| `$ip` | `'0.0.0.0'` | HTTP listen address; Total.run options can override it (index.js). |
| `$unixsocket` | `''` | Alternative Unix socket listen path (index.js). Socket permissions and reverse proxy setup are deployment concerns. |
| `$timezone` | `'utc'` | The time zone of the process (it is set in `process.env.TZ`). |
| `$insecure` | `false` | Configuration sets the process TLS verification environment from this flag (index.js). Keep false for authenticated TLS connections. |
| `$performance` | `false` | Adjusts HTTP agent maximum sockets and socket tuning (index.js); it is not evidence of an application speedup. |
| `$filtererrors` | `true` | Suppresses matching runtime error patterns in the framework error logger (index.js). |
| `$cleartemp` | `true` | Controls startup removal of selected generated/temp file extensions by the clear routine (index.js). |
| `$customtitles` | `false` | When false, the view metadata generator appends the application name to a non-home page title (viewengine.js). |
| `$clearcache` | `10` | Service-counter interval for resetting internal lookup/view/template/query-builder caches (index.js). Not a database deletion schedule. |
| `$imageconverter` | `'gm'` | The program that `Image` calls: `gm`, `im` or `magick`. |
| `$imagememory` | `0` | Disabled because e.g. GM v1.3.32 throws some error about the memory. |
| `$stats` | `true` | Controls statistics output/update branch; selective LOAD can disable it (index.js). |
| `$npmcache` | `'/var/www/.npm'` | Npm cache location passed by NPMINSTALL for the uid-33 execution branch (index.js). |
| `$python` | `'python3'` | Interpreter command used by Python execution and pip helpers (index.js); install Python separately. |
| `$wsmaxsize` | `256` | KB. |
| `$wscompress` | `true` | Enables negotiation of WebSocket permessage-deflate on server connections (websocket.js). |
| `$wsencodedecode` | `false` | Initial WebSocket server encode/decode setting (websocket.js); verify required wire format before changing it. |
| `$wsmaxlatency` | `2000` | Ping-latency threshold in milliseconds used by WebSocket connection checks (websocket.js). |
| `$proxytimeout` | `5` | 5 seconds. |
| `$cookiesamesite` | `'Lax'` | Default SameSite cookie policy when a cookie does not specify one (controller.js). |
| `$cookiesecure` | `false` | Default Secure cookie attribute when no per-cookie override is supplied (controller.js). |
| `$csrfexpiration` | `'30 minutes'` | How long a CSRF token is valid. |
| `$tapi` | `true` | Enables the built-in TotalAPI service driver; the special check operation bypasses this local gate (api.js). |
| `$tapiurl` | `'eu'` | Regional hostname prefix used by the TotalAPI driver (api.js). |
| `$tapimail` | `false` | Selects the TotalAPI mail path when the TotalAPI feature is enabled (mail.js). |
| `$tapilogger` | `false` | Enables the TotalAPI logger path when service access and its secret are configured (index.js). |
| `$imprint` | `true` | Enables the startup banner (index.js). |
| `$tms` | `false` | Turns on the TMS WebSocket endpoint. |
| `$tmsmaxsize` | `256` | The maximum size of a TMS message, in kB. |
| `$tmsurl` | `'/$tms/'` | The URL of the TMS endpoint. |
| `$tmsclearblocked` | `60` | In minutes. |
| `$total5` | set at start | The folder of the `total5` package (the framework reads its own files from it). |
| `$nodemodules` | set at start | The folder where `NPMINSTALL()` installs packages. |
| `secret_uid` | set at start | Secret input used by the short encrypted-identifier utility; initialized from the system hash (index.js, utils.js). |
| `$cryptoiv` | set at start | Initialization-vector buffer used by framework encryption helpers; initialized at startup (index.js, utils.js). |
| `$httpexpire` | set at start | Future UTC HTTP expiry date initialized and periodically refreshed by the framework (index.js, controller.js). |
These keys have no default. They configure the email sending and are used by `MAIL()` and `HTMLMAIL()` (see the email recipe):
| Key | Meaning |
|---|---|
| `mail_smtp` | The address of the SMTP server. |
| `mail_smtp_options` | An object with the rest of the connection settings: `port`, `user`, `password`, `secure`... |
| `mail_from` | The address of the sender. |
| `mail_from_name` | The name of the sender. |
| `mail_reply` | The address that gets the replies, added to every message. |
| `mail_cc` | A copy address, added to every message. |
| `mail_bcc` | A hidden copy address, added to every message. |
```js controllers/config.js
exports.install = function() {
ROUTE('GET /config/', config);
};
function config($) {
$.json({ conf: CONF });
}
```
## Route syntax of Total.js 5
Source: https://forge.totaljs.es/en/docs/route-syntax/
Framework: total5@0.0.19
Content-SHA256: 09125c53800b76738f7a5a0ef26c4af820bdac0879b1cc3eae94133af2f4387c
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`ROUTE(definition, handler)` declares a route. The definition is one text with these parts, in this order:
`[+|-]METHOD /url/{param}/ [@flag ...] [#middleware ...] [ *Action ...]`
| Part | Meaning |
|---|---|
| `METHOD` | `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, or `SOCKET` for a WebSocket and `FILE` for URLs with an extension: a URL such as `/a.txt` is never matched by a `GET` route. |
| `+` or `-` before the method | `+` accepts only requests that `AUTH()` approved, `-` only anonymous requests; any other request is answered with 401. Without a prefix the route is open. |
| `/url/` | The path. Write the end slash: routes are matched with it. |
| `{name}` | A segment of the path that is read from `$.params.name`. |
| `*` as the last segment | Matches any path below it. A route without `*` is more specific and wins. |
| `@flag` | A condition for the request: `@json`, `@xml`, `@xhr`, `@upload`, `@debug`, `@release`, `@mobile`, `@robot`, `@referer` (or `@referrer`). |
| `#name` | A middleware registered with `MIDDLEWARE(name, fn)`. |
| ` *Action` | Runs one or more actions with the input of the request and answers with the result. |
| `#404`, `#401`... | A route whose definition is only `#` and a status code replaces the error page. |
A request is handled by the first matching route. A request that does not meet the flags of a route continues to the next route and ends with a 404 when none matches; a request that does not meet `+` or `-` gets a 401. **A route without flags on the same URL always wins over a route with flags**, so never put both on one URL.
```js definitions/auth.js
AUTH(function($) {
if ($.headers['x-token'] === 'secret')
$.success({ id: '1' });
else
$.invalid();
});
MIDDLEWARE('stamp', function($, next) {
$.response.headers['x-stamp'] = 'yes';
next();
});
NEWACTION('Echo/one', { name: 'one', input: '*text:String', action: ($, model) => $.callback({ one: model.text }) });
NEWACTION('Echo/two', { name: 'two', action: ($, model) => $.callback({ two: true }) });
```
```js controllers/syntax.js
exports.install = function() {
ROUTE('GET /plain/', plain);
ROUTE('GET /users/{id}/', user);
ROUTE('GET /files/*', files);
ROUTE('POST /json/ @json', json);
ROUTE('+GET /private/', plain);
ROUTE('-GET /guest/', plain);
ROUTE('GET /stamped/ #stamp', plain);
ROUTE('POST /echo/ @json --> *Echo/one');
ROUTE('FILE /robots-example.txt', file);
ROUTE('#404', missing);
};
function plain($) {
$.json({ ok: true });
}
function user($) {
$.json({ id: $.params.id });
}
function files($) {
$.json({ url: $.url });
}
function json($) {
$.json({ body: $.body });
}
function file($) {
$.plain('from a FILE route');
}
function missing($) {
$.response.status = 404;
$.plain('custom 404');
}
```
## The $ object of a Total.js 5 handler
Source: https://forge.totaljs.es/en/docs/controller-api/
Framework: total5@0.0.19
Content-SHA256: b04b25b10d7f00afcc531b3bee7c01f76fd782ecbc8281873a3128d66e3c4614
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A route handler receives one object, written `$` in this site. It describes the request and it answers it. Answer **once**: after `$.json()`, `$.html()` or any other answer, the request is finished.
## Properties
| Property | Type | What it is |
|---|---|---|
| `$.url` | string | The path of the request, without the query string. |
| `$.method` | string | `GET`, `POST`... |
| `$.query` | object | The query string, parsed. |
| `$.params` | object | The `{name}` segments of the route. |
| `$.body` | object | The body, parsed by its content type (JSON, URL-encoded form, XML or multipart fields). |
| `$.files` | array | The uploaded files of a multipart request. |
| `$.headers` | object | The headers of the request, with lowercase names. |
| `$.ip` | string | The IP address; the first address of `x-forwarded-for` when the header exists. |
| `$.host` | string | The `host` header. |
| `$.referrer` | string | The `referer` header. |
| `$.protocol` | string | `http` or `https` (also when a proxy says so in `x-forwarded-proto`). |
| `$.ext` | string | The extension of the URL, without the dot (`$.extension` is the same). |
| `$.xhr` | boolean | `true` when the header `x-requested-with` is `XMLHttpRequest`. |
| `$.mobile` | boolean | `true` when the user agent looks like a mobile browser. |
| `$.robot` | boolean | `true` when the user agent looks like a crawler. |
| `$.ua` | string | A short name of the browser and system, such as `iPhone Safari` or `Windows Chrome`. |
| `$.user` | object | The user that `AUTH()` approved, or nothing. |
| `$.language` | string | The language that `LOCALIZE()` chose. |
| `$.datatype` | string | `json`, `urlencoded`, `multipart`, `xml`, `text` or `binary`: the type of the body of the request. |
| `$.response` | object | What will be sent: `status`, `headers` and `value`. Change `$.response.status` and `$.response.headers` before you answer. |
| `$.req`, `$.res` | objects | The Node.js request and response. |
## Methods that answer
| Method | What it does | Recipe |
|---|---|---|
| `$.json(value)` | Answers with JSON. | [responses](/en/total5/responses/) |
| `$.jsonstring(text)` | Answers with a text that is already JSON, without serializing it again. | [controller-completion-and-cache](/en/total5/controller-completion-and-cache/) |
| `$.html(text)` | Answers with HTML. | [responses](/en/total5/responses/) |
| `$.plain(text)` | Answers with plain text (`$.text` is the same). | [responses](/en/total5/responses/) |
| `$.xml(text)` | Answers with XML. | [responses](/en/total5/responses/) |
| `$.empty()` | Answers with status 204 and no body. | [responses](/en/total5/responses/) |
| `$.redirect(url, permanent)` | Redirects (302, or 301 when `permanent` is true). | [responses](/en/total5/responses/) |
| `$.binary(buffer, type, name)` | Answers with bytes and a content type. | [responses](/en/total5/responses/) |
| `$.file(path, name)` | Sends a file from disk. | [file-download](/en/total5/file-download/) |
| `$.stream(type, stream, name)` | Sends a readable stream. | [file-download](/en/total5/file-download/) |
| `$.filefs(storage, id)` | Sends a file of a `FILESTORAGE`. | [filestorage](/en/total5/filestorage/) |
| `$.image(options)` | Creates and sends an image on demand. | [images](/en/total5/images/) |
| `$.view(name, model)` | Renders a view with the layout and answers with it. | [views-and-layouts](/en/total5/views-and-layouts/) |
| `$.layout(name)` | Chooses another layout for the next `$.view()`. | [view-sections](/en/total5/view-sections/) |
| `$.proxy(url)` | Forwards the request to another URL and answers with its response. | [proxy](/en/total5/proxy/) |
| `$.respond(value)` | Answers in the format of `$.response.output` (`json` when it is not set). | [controller-completion-and-cache](/en/total5/controller-completion-and-cache/) |
| `$.invalid(value)` | Answers with an error: a text, a status code such as `404` or an `ErrorBuilder`. | [actions-crud](/en/total5/actions-crud/) |
| `$.fallback(code)` | Answers with the page of an error code, the custom one when `ROUTE('#404')` exists. | [error-pages](/en/total5/error-pages/) |
| `$.success(value)` | Answers `{ "success": true }`; `value` is added to it. | [responses](/en/total5/responses/) |
| `$.callback(err, value)` | With an error answers `$.invalid(err)`, otherwise `$.json(value)`, or `$.success()` when `value` is `undefined`. Without arguments it returns a function `(err, response)` that does the same. | [actions-crud](/en/total5/actions-crud/) |
| `$.successful(fn)` | Returns a function `(err, a, b, c)`: with an error it answers `$.invalid(err)`, otherwise it calls `fn(a, b, c)`. | [controller-completion-and-cache](/en/total5/controller-completion-and-cache/) |
| `$.done(arg)` | Returns a function `(err, response)`: with an error it answers `$.invalid(err)`, otherwise it answers with the success object. | [controller-completion-and-cache](/en/total5/controller-completion-and-cache/) |
## Other methods
| Method | What it does | Recipe |
|---|---|---|
| `$.cookie(name, value, expiration)` | Reads a cookie, or writes one when you pass a value. | [headers-and-cookies](/en/total5/headers-and-cookies/) |
| `$.csrf()` | Creates a CSRF token for this client. | [csrf-protection](/en/total5/csrf-protection/) |
| `$.hostname(path)` | The address of this server: protocol, host and the path you pass. | [restbuilder](/en/total5/restbuilder/) |
| `$.action(name, model)` | Runs an action with this request (user, query and params). | [calling-actions-from-code](/en/total5/calling-actions-from-code/) |
| `$.transform(name, value, callback)` | Runs a transform with this request. | [transforms](/en/total5/transforms/) |
| `$.authorize(callback)` | Runs `AUTH()` for this request and sets `$.user`; it returns a promise when there is no callback. | [controller-completion-and-cache](/en/total5/controller-completion-and-cache/) |
| `$.notmodified(date)` | Answers 304 and returns `true` when the request has the header `if-modified-since` with that date. | [controller-completion-and-cache](/en/total5/controller-completion-and-cache/) |
| `$.httpcache(date)` | Sets the cache headers and `last-modified` of the answer. | [controller-completion-and-cache](/en/total5/controller-completion-and-cache/) |
| `$.clear()` | Removes the temporary files of the upload. They are removed anyway when the request ends. | [file-upload](/en/total5/file-upload/) |
| `$.autoclear(false)` | Keeps the uploaded files after the request, so that you can use them later. | [upload-lifecycle-and-href](/en/total5/upload-lifecycle-and-href/) |
| `$.custom()` | Tells the framework that you answer through `$.res` yourself: it will not send anything. | [controller-completion-and-cache](/en/total5/controller-completion-and-cache/) |
| `$.href(key, value)` | Use `QUERIFY()` as in the linked recipe, the tested native query-link helper. | [upload-lifecycle-and-href](/en/total5/upload-lifecycle-and-href/) |
| `$.flush()`, `$.resume()`, `$.free()`, `$.cancel()` | Internal methods: do not call them. | - |
The example answers with the properties and calls the methods that need no setup.
```js controllers/api.js
exports.install = function() {
ROUTE('GET /info/{a}/{b}/', info);
ROUTE('POST /body/ @json', body);
ROUTE('GET /answers/{kind}/', answers);
ROUTE('GET /callback/{kind}/', callback);
};
function info($) {
$.json({
url: $.url,
method: $.method,
query: $.query,
params: $.params,
ext: $.ext,
xhr: $.xhr,
mobile: $.mobile,
robot: $.robot,
ua: $.ua,
ip: $.ip,
host: $.host,
referrer: $.referrer,
protocol: $.protocol,
hostname: $.hostname('/x/'),
datatype: $.datatype
});
}
function body($) {
$.json({ body: $.body, datatype: $.datatype });
}
function answers($) {
switch ($.params.kind) {
case 'json': return $.json({ a: 1 });
case 'jsonstring': return $.jsonstring('{"a":1}');
case 'html': return $.html('x');
case 'plain': return $.plain('x');
case 'xml': return $.xml('1');
case 'empty': return $.empty();
case 'redirect': return $.redirect('/x/', true);
case 'binary': return $.binary(Buffer.from('abc'), 'application/octet-stream');
case 'invalid': return $.invalid(409);
case 'success': return $.success({ id: 7 });
case 'status': $.response.status = 201; return $.json({ created: true });
case 'header': $.response.headers['x-example'] = 'yes'; return $.json({});
}
}
function callback($) {
switch ($.params.kind) {
case 'error': return $.callback('failed');
case 'value': return $.callback(null, { v: 1 });
case 'nothing': return $.callback(null);
case 'function': return setImmediate($.callback(), null, { later: true });
case 'successful': return setImmediate($.successful(function(a, b) { $.json({ a: a, b: b }); }), null, 'x', 'y');
}
}
```
## Options of NEWACTION and the object an action receives
Source: https://forge.totaljs.es/en/docs/action-options/
Framework: total5@0.0.19
Content-SHA256: f1ffd2184b29e3078bcf6ecaeeb9ef9f5f0c5e6e808b6ea368164415466f662b
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`NEWACTION(name, options)` declares an action. `name` is its id (`Users/insert`): routes use it with `--> *Users/insert` and your code with `ACTION('Users/insert', payload)`.
## Options
| Option | Value | What it does |
|---|---|---|
| `name` | text | A description of the action; it is also available as `$.name`. |
| `action` | `function($, model)` | The code of the action. `model` is the validated input. |
| `input` | text | The fields of the body, as `name:Type` separated by commas; a `*` before the name makes it required. See the input types page. |
| `output` | text | The fields of the answer; fields that are not listed are removed. |
| `params` | text | The `{name}` segments of the route, validated like `input`. |
| `query` | text | The query string, validated like `input`. |
| `partial` | `true` | Only the fields that were sent reach the action. Use it for `PATCH`. |
| `user` | `true` or `false` | `true` answers 401 when there is no signed-in user; `false` accepts only anonymous requests. |
| `permissions` | text or array | `'read,write'`: the user needs one of them (or to be a super administrator). |
| `route` | text | Declares the route of the action here: `'GET /products/'`, or `true` for an `API` route. |
| `middleware` | text | Names of middleware that run before the action, separated by spaces or commas. |
| `timeout` | milliseconds | When the action does not answer in time, the request fails with the error `Timeout.`. |
| `cache` | `true` or an object | Keep the result of an action with `CACHE`. |
| `config` | object | Values that the action reads from `$.config`. |
| `audit` | text | Records an automatic event after successful completion; see the dedicated recipe for tested boundaries. [action-audit](/en/total5/action-audit/) |
| `csrf`, `encrypt`, `compress`, `publish` | - | [action-transport-boundaries](/en/total5/action-transport-boundaries/) |
| `sa`, `su` | true | Either requires a user with either administrator flag. [action-hooks-and-admin](/en/total5/action-hooks-and-admin/) |
| `oninput`, `onoutput` | function | Before the handler after validation; successful completion hook after output processing. [action-hooks-and-admin](/en/total5/action-hooks-and-admin/) |
| `extend` | path | Adds the result under this path in the validated input and returns the extended payload. [action-hooks-and-admin](/en/total5/action-hooks-and-admin/) |
## The object `$` of an action
| Member | What it is |
|---|---|
| `$.query`, `$.params` | The validated query string and route segments. |
| `$.payload` | The validated input; it is the same object as the second argument of `action`. |
| `$.user` | The signed-in user. |
| `$.config` | The `config` option. |
| `$.name`, `$.id` | The `name` option and the id of the action. |
| `$.controller` | The `$` of the route when the action was called from a route, so `$.controller.body` is the raw body. It does not exist when the action is called with `ACTION()`. |
| `$.callback(value)` | Answers with a value; it is filtered by `output`. Without arguments it returns a function `(err, response)`. |
| `$.success(value)` | Answers `{ success: true }`. |
| `$.invalid(error)` | Answers with an error: a text, a status code or an `ErrorBuilder`. |
| `$.done(arg)`, `$.successful(fn)` | Return callbacks for functions that report an error as their first argument. |
| `$.cookie(name, value, expiration)` | Reads or writes a cookie of the request. |
| `$.redirect(url)` | Redirects the request. |
| `$.output(name)` | Chooses how a route answers: `json`, `html`, `xml`, `text`, `redirect`, `binary`, `jsonstring`, `empty` or `file`. |
| `$.unauthorized(...permissions)` | Answers 401 and returns `true` when the user has none of the permissions. |
| `$.action(name, payload)` | Runs another action with the same request. |
| `$.hostname(path)` | The address of this server. |
```js definitions/options.js
MIDDLEWARE('tag', function($, next) {
$.response.headers['x-tag'] = 'on';
next();
});
var counter = { cached: 0 };
MAIN.counter = counter;
NEWACTION('Options/named', {
name: 'The name',
config: { limit: 3 },
action: function($) {
$.callback({ name: $.name, id: $.id, limit: $.config.limit });
}
});
NEWACTION('Options/guarded', {
name: 'Needs a user',
user: true,
permissions: 'read',
action: function($) {
$.success();
}
});
NEWACTION('Options/slow', {
name: 'Never answers',
timeout: 300,
action: function($) {}
});
NEWACTION('Options/cached', {
name: 'The cache option',
query: 'page:Number',
cache: { expire: '1 minute', query: 'page' },
action: function($) {
counter.cached++;
$.callback({ page: $.query.page, runs: counter.cached });
}
});
NEWACTION('Options/routed', {
name: 'Declares its own route',
route: 'GET /routed/ #tag',
action: function($) {
$.callback({ routed: true });
}
});
NEWACTION('Options/output', {
name: 'Chooses how to answer',
action: function($) {
$.output('plain');
$.callback('plain text');
}
});
AUTH(function($) {
if ($.headers['x-user'])
$.success({ id: '1', permissions: $.headers['x-user'].split(',') });
else
$.invalid();
});
```
```js controllers/options.js
exports.install = function() {
ROUTE('GET /named/ --> *Options/named');
ROUTE('GET /guarded/ --> *Options/guarded');
ROUTE('GET /slow/ --> *Options/slow');
ROUTE('GET /cached/ --> *Options/cached');
ROUTE('GET /output/ --> *Options/output');
};
```
## Total.js 5 API guide by task
Source: https://forge.totaljs.es/en/docs/total5-coverage-map/
Framework: total5@0.0.19
Content-SHA256: aecbccee658ac585644f027f210437bc8b284f99b0ce3dd199ee4bd88bd5a667
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Use these references to choose the native Total.js 5 API for your task. The examples target total5@0.0.19.
## Native backend references
| Area | Reference and examples |
|---|---|
| Installation, directories and startup | [getting started](/en/docs/getting-started/), [project structure](/en/docs/project-structure/), [LOAD and NPMINSTALL](/en/total5/load-and-npminstall/), [application bundles](/en/total5/application-bundles/), [release/debug modes](/en/total5/release-mode-and-debugging/) |
| Global API and configuration | [globals](/en/docs/globals-total5/), [configuration keys](/en/docs/configuration-keys/), [configuration files](/en/total5/configuration-file/) |
| Routing and controller | [route syntax](/en/docs/route-syntax/), [controller API](/en/docs/controller-api/), [completion and conditional responses](/en/total5/controller-completion-and-cache/), [upload lifecycle and query links](/en/total5/upload-lifecycle-and-href/), [static cache invalidation](/en/total5/touch-static-cache/) |
| Actions and validation | [action reference](/en/docs/action-options/), [input types](/en/docs/input-types/), [enums and format boundaries](/en/total5/schema-enums-and-formats/), [numeric boundaries](/en/total5/numeric-schema-boundaries/), [hooks and admin guards](/en/total5/action-hooks-and-admin/), [transport options](/en/total5/action-transport-boundaries/), [audit](/en/total5/action-audit/) |
| Database queries | [DATA](/en/total5/data-api/), [introductory DB/NEWDB](/en/total5/query-builder-db/), [builder operations and driver contract](/en/total5/query-builder-contract/), [real SQLite CRUD and transactions](/en/total5/query-builder-sqlite/), [NoSQL](/en/total5/nosql-crud/), [file storage](/en/total5/filestorage/) |
| Messaging and platform protocols | [WebSockets](/en/total5/websocket-chat/), [TMS](/en/total5/tms/), [OPENCLIENT protocol fixture](/en/total5/openclient-protocol/), [real OpenLDAP fixture](/en/total5/ldap-openldap/) |
| Flow and executable components | [FlowStream](/en/total5/flowstream/), [component testing](/en/total5/flowstream-component-test/), [flow files and workers](/en/total5/flow-files-and-workers/), [native server components](/en/total5/native-components/) |
| Optional remote services | [API drivers](/en/total5/api-drivers/), [TotalAPI and Componentator local boundaries](/en/total5/remote-service-boundaries/) |
| Full application | [Taskboard reference](/en/docs/taskboard-reference/): sessions, CSRF, owner-scoped NoSQL CRUD and browser assertions; exported app has separate restart/revocation checks |
Additional existing recipes cover views/localization, email, encryption helpers, caching, file downloads, proxies, jobs, threads, HTTP clients, macros, image conversion, AI-model driver contracts and Python processes. Use the Total5 catalogue and per-page declared scope for their exact behavior. Python scripts, image converter programs and directory servers are separate environment requirements, not npm dependencies of the framework core.
## Which QueryBuilder?
The native DB/NEWDB API builds an options object for a registered driver. DATA is the singleton entry point for independent operations; DB creates a queue. Queue ordering is not a transaction. The SQLite recipe implements a bounded application-owned driver and explicit connection-owned transactions.
The optional [official querybuilderpg integration](https://github.com/totaljs/querybuilderpg) is a separate PostgreSQL package. Initialize a named connection with its connection string and pooling options, then use DATA. The application-owned SQLite driver is specific to SQLite.
## Behavior in total5@0.0.19
Use these version-specific behaviors when writing application code:
- Compact Int accepts fractions and Number2 does not round to two places; SmallInt bounds differ from the familiar SQL range.
- Compact JSON and IP labels are read as plain strings; use explicit native JSON Schema subtypes when you need those formats enforced.
- For action csrf/encrypt/compress metadata, use explicit CSRF middleware and test the actual transport.
- For LDAP, decide authentication from your own check of the bind result.
- For query links use QUERIFY with a copied query.
- For QueryBuilder column selection, use fields with an application allowlist.
## Input types of NEWACTION
Source: https://forge.totaljs.es/en/docs/input-types/
Framework: total5@0.0.19
Content-SHA256: 44656a0df7eb097a27c8aa84184977e03028e29192b75dd120a80d0888d881bc
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A definition is a list of `name:Type` separated by commas, for example `*email:Email, age:Number`. A `*` before the name makes the field required. The same format is used by `input` (the body), `params`, `query` and `output`. **The type converts the value**: the action receives the converted value, not the text that was sent.
The names of the types are not case sensitive. Every type below was checked by sending the value in the second column.
| Type | Value sent | Value received | Notes |
|---|---|---|---|
| `String` | ` hello ` | `hello` | Text; leading and trailing spaces are removed. |
| `Text` | ` text ` | ` text ` | Text kept exactly as it was sent. |
| `Email` | ` A@B.CZ ` | `a@b.cz` | A valid email address, in lowercase; anything else is rejected. |
| `Phone` | `+421 900 123 456` | `+421900123456` | A phone number; spaces are removed. |
| `URL` | `https://totaljs.com/x` | `https://totaljs.com/x` | A valid URL; anything else is rejected. |
| `Name` | `john smith` | `John Smith` | Capitalizes every word. |
| `Capitalize` | `hello big world` | `Hello Big World` | Capitalizes every word. |
| `Capitalize2` | `HELLO big WORLD` | `Hello big world` | Capitalizes the first letter only and lowercases the rest. |
| `Lowercase` | `ABC` | `abc` | Lowercase. |
| `Uppercase` | `abc` | `ABC` | Uppercase. |
| `Search` | `Žluťoučký Kůň` | `zlutoucki kun` | Lowercase without accents, for searching. |
| `SafeString` | `plain text` | `plain text` | Text; a value with HTML tags such as `` is rejected. |
| `Color` | `#FF0000` | `#FF0000` | [schema-enums-and-formats](/en/total5/schema-enums-and-formats/) |
| `Base64` | `SGVsbG8=` | `SGVsbG8=` | [schema-enums-and-formats](/en/total5/schema-enums-and-formats/) |
| `Zip` | `81101` | `81101` | [schema-enums-and-formats](/en/total5/schema-enums-and-formats/) |
| `Icon` | `ti ti-home` | `ti ti-home` | [schema-enums-and-formats](/en/total5/schema-enums-and-formats/) |
| `Number` | `12.5` | `12.5` | A number; the text is converted. |
| `Float` | `3.14` | `3.14` | A number; the text is converted. |
| `Decimal` | `2.5` | `2.5` | A number; the text is converted. |
| `Boolean` | `true` | `true` | `true` or `false`; the text `true` is converted. |
| `Date` | `2026-01-05` | `2026-01-05T00:00:00.000Z` | A date. In JSON it is sent as an ISO text. |
Other forms:
| Form | Example | Notes |
|---|---|---|
| `[String]`, `[Number]` | `ze_arr:[String]` | An array of that type: every item is converted. |
| `{ x:Number, y:String }` | `zg_obj:{ x:Number, y:String }` | A nested object, validated with its own fields. |
| `String(5)` | `zd_max:String(5)` | A text of at most 5 characters; a longer text is rejected. |
A value that does not fit the type is rejected with status 400 and the name of the field in the error. These values were rejected in the checks: `SafeString` with `x`, `String(5)` with ten characters, a `UID` with `abc`, and a `GUID` with `not-guid`. The boundary recipe verifies `Int`, `SmallInt`, `TinyInt`, `Number2`: [numeric-schema-boundaries](/en/total5/numeric-schema-boundaries/). In this version `Int` does not truncate: `7.9` stays `7.9`.
`UID` and `GUID` accept the values that `UID()` and `GUID()` create.
```js definitions/types.js
NEWACTION('Types/all', {
name: 'Every type',
input: 'a_string:String, b_text:Text, c_email:Email, d_phone:Phone, e_url:URL, f_name:Name, g_capitalize:Capitalize, h_capitalize2:Capitalize2, i_lowercase:Lowercase, j_uppercase:Uppercase, k_search:Search, l_safestring:SafeString, m_color:Color, n_base64:Base64, o_zip:Zip, p_icon:Icon, q_number:Number, r_float:Float, s_decimal:Decimal, t_boolean:Boolean, u_date:Date, ze_arr:[String], zf_nums:[Number], zg_obj:{ x:Number, y:String }, zd_max:String(5)',
action: function($, model) {
$.callback(model);
}
});
NEWACTION('Types/ids', {
name: 'UID and GUID',
input: '*id:UID, *key:GUID',
action: function($, model) {
$.callback(model);
}
});
```
```js controllers/types.js
exports.install = function() {
ROUTE('POST /types/ @json --> *Types/all');
ROUTE('POST /ids/ @json --> *Types/ids');
ROUTE('GET /newids/', ids);
};
function ids($) {
$.json({ id: UID(), key: GUID() });
}
```
## Global names of jComponent 20
Source: https://forge.totaljs.es/en/docs/globals-jcomponent/
Framework: jcomponent@20
Content-SHA256: 475033e653e583e6bfd9374c95487eef789587c4d321284b4df134d1eb32018b
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
jComponent 20 exposes global functions and objects for state, components, requests and browser utilities. The bundle also includes jQuery. Use the table to find a name and its usage recipe.
| Name | Kind | What it does | Recipe |
|---|---|---|---|
| `$` | function | jQuery. | [getting-started](/en/jcomponent/getting-started/) |
| `$totaluidate` | function | Internal: the hour minus 12 from noon on (`$totaluidate(13)` is `1`, `$totaluidate(5)` stays `5`). | [data-helpers](/en/jcomponent/data-helpers/) |
| `$W` | object | The jQuery object of the window. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `ADAPT` | function | Replaces the markers (`~PATH~`, `~ID~`, `CLASS`) and translates the text of a component. | [find-and-call](/en/jcomponent/find-and-call/) |
| `AJAX` | function | `AJAX('METHOD /url/', data, callback)` makes a request to the server. | [ajax](/en/jcomponent/ajax/) |
| `API` | function | Calls an `ROUTE('API ...')` endpoint of Total.js with a schema name and data. | [api-calls](/en/jcomponent/api-calls/) |
| `APPEARANCE` | function | Sets the color, the dark mode and the CSS variables of the page. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `ASETTER` | function | Returns a function that calls `SETTER()` later. | [find-and-call](/en/jcomponent/find-and-call/) |
| `ATTRD` | function | Reads a `data-` attribute of an element or an event. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `BLOCKED` | function | Returns false the first time and true during a time: a guard against repeated actions. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `CACHE` | function | Stores a value in the browser for a period. | [storage-cache-pref-cookies](/en/jcomponent/storage-cache-pref-cookies/) |
| `CACHEPATH` | function | Keeps a path of the state in the cache and restores it. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `CL` | function | Loads the code lists that are registered with `CLINIT()` and are not loaded yet. | [data-helpers](/en/jcomponent/data-helpers/) |
| `clearTimeout2` | function | Cancels a timer of `setTimeout2()`. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `CLINIT` | function | Registers a code list and the function that loads it. | [data-helpers](/en/jcomponent/data-helpers/) |
| `CLONE` | function | Deep copy of a value, dates included. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `CLRELOAD` | function | Loads code lists again. | [data-helpers](/en/jcomponent/data-helpers/) |
| `CMD` | function | Runs a command of a component. Use the `CMD` of an instance or `EXEC`. | [commands](/en/jcomponent/commands/) |
| `COMPILE` | function | The same function as `NOOP` in jComponent 20. | [find-and-call](/en/jcomponent/find-and-call/) |
| `COMPONENT` | function | Declares a component. | [custom-component](/en/jcomponent/custom-component/) |
| `COMPONENTS` | function | The ready components under a path. | [find-and-call](/en/jcomponent/find-and-call/) |
| `CONFIG` | function | Changes the configuration of components found by a selector. | [component-config](/en/jcomponent/component-config/) |
| `COOKIES` | object | Reads, writes and removes cookies. | [storage-cache-pref-cookies](/en/jcomponent/storage-cache-pref-cookies/) |
| `COPY` | function | Copies the properties of an object into another one, deep. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `DAYS` | object | The names of the days of the week in English, from Sunday. | [data-helpers](/en/jcomponent/data-helpers/) |
| `DECRYPT` | function | Reads a value made by `ENCRYPT()` with the same key and type. | [data-helpers](/en/jcomponent/data-helpers/) |
| `DEF` | object | Defaults of jComponent. `DEF.fallback` is the address from which components that are not loaded are downloaded. | - |
| `DIFFDOM` | function | Makes the children of an element the same as a new list without redrawing the ones that stay. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `EMIT` | function | Emits a global event. | [events](/en/jcomponent/events/) |
| `EMPTYARRAY` | object | A frozen empty array. | [data-helpers](/en/jcomponent/data-helpers/) |
| `EMPTYOBJECT` | object | A frozen empty object. | [data-helpers](/en/jcomponent/data-helpers/) |
| `ENCRYPT` | function | Turns a value into hexadecimal text with a check of the key (obfuscation, not security). | [data-helpers](/en/jcomponent/data-helpers/) |
| `ENV` | function | Registers or reads a value of the page, used in URLs as `[name]`. | [environment-variables](/en/jcomponent/environment-variables/) |
| `ENVIRONMENT` | function | Sets the name of the application for the storage, the version and the language. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `ERROR` | function | Helper to handle the validation errors of an `API()` answer. | [api-calls](/en/jcomponent/api-calls/) |
| `ERRORS` | function | Returns the invalid components under a path. | [validation](/en/jcomponent/validation/) |
| `EXE` | function | Calls a plugin method, a global function or a function of the state (the same as `EXEC()`). | [find-and-call](/en/jcomponent/find-and-call/) |
| `EXEC` | function | Calls the method of a plugin (`EXEC('plugin/method')`) or a global function. | [commands](/en/jcomponent/commands/) |
| `EXTENSION` | function | Adds code to every instance of a component or plugin. | [extensions](/en/jcomponent/extensions/) |
| `FIND` | function | Finds a ready component by name, path or id. | [find-and-call](/en/jcomponent/find-and-call/) |
| `FLAGS` | function | Dispatches custom path flags and invokes a callback. | [custom-flags](/en/jcomponent/custom-flags/) |
| `FN` | function | Compiles an expression into a function and remembers it. | [data-helpers](/en/jcomponent/data-helpers/) |
| `FREE` | function | Runs the cleanup of removed components at once. | [component-lifecycle](/en/jcomponent/component-lifecycle/) |
| `FUNC` | object | An object for functions that `EXE('FUNC.name')` can call. | [find-and-call](/en/jcomponent/find-and-call/) |
| `GET` | function | Reads a value from a path of the state. | [state-api](/en/jcomponent/state-api/) |
| `GUID` | function | Returns a random id. | [utilities](/en/jcomponent/utilities/) |
| `HASH` | function | Returns a numeric hash. | [utilities](/en/jcomponent/utilities/) |
| `HIDDEN` | function | True when an element is not shown. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `IMPORT` | function | Loads an HTML part, a script or a style from a URL. | [ui-import](/en/jcomponent/ui-import/) |
| `INC` | function | Adds 1 (or the given number) to the number in a path. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `inDOM` | function | True while an element is in the document. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `isIE` | boolean | True in Internet Explorer. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `isMOBILE` | boolean | True on a mobile device. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `isPRIVATEMODE` | boolean | True in a private window. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `isROBOT` | boolean | True for a search engine robot. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `isSTANDALONE` | boolean | True when the page runs as an installed application. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `isTOUCH` | boolean | True on a device with touch. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `jComponent` | object | The engine of jComponent: `M.components`, `M.version`... | [find-and-call](/en/jcomponent/find-and-call/) |
| `jQuery` | function | jQuery, bundled in the same file. | [getting-started](/en/jcomponent/getting-started/) |
| `LOCALIZE` | function | Loads a dictionary in the format of the Total.js resource files. | [data-helpers](/en/jcomponent/data-helpers/) |
| `M` | object | The engine of jComponent (the same object as `jComponent`, `Total` and `MAIN`). | [find-and-call](/en/jcomponent/find-and-call/) |
| `MAIN` | object | The engine of jComponent (the same object as `jComponent`, `Total` and `M`). | [find-and-call](/en/jcomponent/find-and-call/) |
| `MIDDLEWARE` | function | Registers a function that runs before a route, or runs a list of them in order. | [find-and-call](/en/jcomponent/find-and-call/) |
| `MONTHS` | object | The names of the months in English. | [data-helpers](/en/jcomponent/data-helpers/) |
| `NAV` | object | The current location: `url`, `query`, `params`, and `back()`/`forward()`. | [spa-routing](/en/jcomponent/spa-routing/) |
| `NEWUIBIND` | function | Makes a `ui-bind` on an element from the code. | [find-and-call](/en/jcomponent/find-and-call/) |
| `NODEINDEXOF` | function | The position of an element among its siblings. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `NODEINSERT` | function | Moves an element after or before another one. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `NODEMOVE` | function | Moves an element one place down or up in its parent. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `NOOP` | function | A function that does nothing. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `NOTFOCUSED` | function | True when the browser tab is in the background. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `NOW` | object | A `Date` that jComponent keeps up to date. | [data-helpers](/en/jcomponent/data-helpers/) |
| `NUL` | function | Sets the value of a path to `null` (the same as `NULL`). | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `NULL` | function | Sets the value of a path to `null`. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `OFF` | function | Removes listeners of a global event. | [events](/en/jcomponent/events/) |
| `ON` | function | Registers a listener for a global event. | [events](/en/jcomponent/events/) |
| `PARSE` | function | Parses JSON and turns date strings into dates. | [utilities](/en/jcomponent/utilities/) |
| `PIPE` | function | `API()` to the address in `DEF.pipe`. | [find-and-call](/en/jcomponent/find-and-call/) |
| `PLUGIN` | function | Declares a plugin. | [plugin](/en/jcomponent/plugin/) |
| `PLUGINS` | object | The plugins of the page by their path. | [find-and-call](/en/jcomponent/find-and-call/) |
| `PREF` | object | User preferences saved in the browser. | [storage-cache-pref-cookies](/en/jcomponent/storage-cache-pref-cookies/) |
| `PUSH` | function | Adds a value to an array of the state and notifies. | [state-api](/en/jcomponent/state-api/) |
| `QUERIFY` | function | Makes a query string from an object, also after an address or a method. | [data-helpers](/en/jcomponent/data-helpers/) |
| `REDIRECT` | function | Changes the address without loading the page and runs the matching route. | [spa-routing](/en/jcomponent/spa-routing/) |
| `REPO` | object | An object of yours to share data between components. | [find-and-call](/en/jcomponent/find-and-call/) |
| `RESET` | function | Clears the touched and modified states under a path. | [validation](/en/jcomponent/validation/) |
| `ROUTE` | function | Registers a client-side route. | [spa-routing](/en/jcomponent/spa-routing/) |
| `SCROLLBAR` | function | Wraps an element with a custom scrollbar. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `SCROLLBARWIDTH` | function | The width that a scrollbar takes. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `SEEX` | function | Calls a function by name or sets a path. | [find-and-call](/en/jcomponent/find-and-call/) |
| `SET` | function | Writes a value at a path of the state and notifies everything bound to it. | [state-api](/en/jcomponent/state-api/) |
| `SETTER` | function | Calls a method of every component with a name, path or id. | [find-and-call](/en/jcomponent/find-and-call/) |
| `setTimeout2` | function | A debounced `setTimeout()` with an id and an optional limit of restarts. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `STRINGIFY` | function | `JSON.stringify()` that can compress empty values and choose the fields. | [data-helpers](/en/jcomponent/data-helpers/) |
| `STYLE` | function | Adds a style to the head; the same id replaces it. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `Ta` | object | The same object as `Tangular`, the template engine. | [data-helpers](/en/jcomponent/data-helpers/) |
| `Tangular` | object | The template engine. | [ui-bind-templates](/en/jcomponent/ui-bind-templates/) |
| `TAPI` | function | `API()` to the address in `DEF.api`. | [find-and-call](/en/jcomponent/find-and-call/) |
| `TEMP` | object | An object for temporary data. | [find-and-call](/en/jcomponent/find-and-call/) |
| `Thelpers` | object | The helpers of the templates (`Tangular.helpers`): add one with `Thelpers.name = fn`. | [data-helpers](/en/jcomponent/data-helpers/) |
| `TOGGLE` | function | Changes the boolean in a path. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `Total` | object | The engine of jComponent (the same object as `jComponent`). | [find-and-call](/en/jcomponent/find-and-call/) |
| `TRANSLATE` | function | Replaces every `@(text)` with its translation. | [data-helpers](/en/jcomponent/data-helpers/) |
| `UNAUTHORIZED` | function | True when `window.user` has none of the given permissions. | [data-helpers](/en/jcomponent/data-helpers/) |
| `UNWATCH` | function | Removes the watchers of a path made with `WATCH()`. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `UPD` | function | Notifies a path after you changed the object in place. | [state-api](/en/jcomponent/state-api/) |
| `UPDATE` | function | Notifies the components and the watchers of a path without changing it (alias `UPD`). | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `UPLOAD` | function | Sends a `FormData` to a route with progress. | [upload](/en/jcomponent/upload/) |
| `VISIBLE` | function | True when an element is in the visible part of the window. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `W` | object | The `window`. | [find-and-call](/en/jcomponent/find-and-call/) |
| `WAIT` | function | Runs a function when another one returns true or a path has a value; checks every 500 ms. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `WARN` | function | Writes to `console.warn`. | [state-shortcuts](/en/jcomponent/state-shortcuts/) |
| `WATCH` | function | Runs a function every time a path changes. | [state-api](/en/jcomponent/state-api/) |
| `WH` | number | The height of the window. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `WIDTH` | function | The size class of the width of an element or the window: xs, sm, md or lg. | [dom-helpers](/en/jcomponent/dom-helpers/) |
| `WW` | number | The width of the window. | [dom-helpers](/en/jcomponent/dom-helpers/) |
```html public/index.html
jComponent
The test lists the globals of this page.
```
## Total.js strengths and the application problems they solve
Source: https://forge.totaljs.es/en/docs/totaljs-strengths/
Framework: total5@0.0.19
Content-SHA256: 4ac399045c8774737de1e5d46188c4230b57be3a7a260c4c950ac9c93f76cef6
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js is much more than HTTP routing. Its central strength is a dependency-free framework core that brings the whole backend together under shared conventions: routes, validation, authentication, views, uploads, realtime, scheduled work and more. For a team building APIs, business applications or live dashboards, that means fewer separately selected packages, less integration code and a project that stays easy to maintain as it grows.
## What Total.js offers
| Strength | What it gives an application | Source or example |
|---|---|---|
| Framework core without third-party npm dependencies | The installed `total5@0.0.19` package declares no external runtime dependencies. No tree of modules from different authors to keep compatible, and no module that stops being maintained and leaves your project stranded. This protects the continuity and the scalability of a project in a very direct way. | [Framework source and package manifest](https://github.com/totaljs/framework5), [official platform description](https://www.totaljs.com/) |
| Integrated backend facilities | Routing, schemas, views, uploads, scheduled jobs, email and localization share one set of conventions. A business application covers many requirements without assembling a separate library for each one. | [Project structure](/en/docs/project-structure/), [uploads](/en/total5/file-upload/), [cron](/en/total5/cron-jobs/), [email](/en/total5/send-email/), [localization](/en/total5/localization/) |
| Robust input validation and authentication | Required fields, input contracts, protected routes, signed sessions and CSRF protection are native building blocks, so security starts built in instead of assembled from add-ons. | [Input validation](/en/total5/validate-json-body/), [protected routes](/en/total5/protect-routes-with-auth/), [sessions](/en/total5/sessions-with-built-in-auth/) |
| Realtime communication | WebSocket server and client facilities power bidirectional applications such as live dashboards, notifications and connected services. | [WebSocket server](/en/total5/websocket-chat/), [WebSocket client](/en/total5/websocket-client/) |
| FlowStream in the backend | Connected components let an application express message-processing workflows and change component configuration while running: ideal for automation and integration pipelines. | [FlowStream example](/en/total5/flowstream/), [component test](/en/total5/flowstream-component-test/) |
| Native Python process integration | `PYPELINE` is a framework-managed path to start Python scripts and exchange data. Your Node.js application keeps its API and orchestration while Python handles a specialist task. | [PYPELINE entry point](https://github.com/totaljs/framework5/blob/main/global.js), [implementation](https://github.com/totaljs/framework5/blob/main/pypeline.js) |
| Server-rendered pages in the same framework | Views, layouts, includes and localization let one application serve HTML next to its APIs and realtime endpoints. | [Views and layouts](/en/total5/views-and-layouts/), [includes and assets](/en/total5/view-includes-and-assets/) |
| Direct routes and reusable actions | A small endpoint answers from its controller; a domain operation uses an action and its validated contract. You choose the layer each piece needs. | [Direct controller example](/en/docs/choosing-totaljs/), [native action example](/en/docs/totaljs-agent-conventions/) |
| Open-source framework under MIT | You can read the implementation, learn from it and adapt it under the license terms, including for commercial applications. | [Framework license](https://github.com/totaljs/framework5/blob/main/LICENSE) |
## Python interoperability: a concrete capability
In the `total5@0.0.19` source, `PYPELINE(name, options)` resolves named scripts from `pypelines/` or accepts inline Python. It starts a `python3` process and exchanges messages over a local socket, supporting JSON, text and binary data. This is a built-in bridge between the Node.js application and Python processing, so data science, imaging or machine-learning code can sit next to your API without a separate service.
Python needs to be installed in the target environment, and a script may use Python packages of its own, as with any Python project.
## Fast and light
Total.js is fast, and it is light: an all-in-one core without a dependency tree means fewer packages to install, load, update and audit, which counts most on small servers and small devices. Forge measures it under controlled conditions and publishes latency, CPU and memory for every workload, with hardware, versions, raw data and downloadable archives, in the [benchmarks](/en/benchmarks/), so you can see the numbers and reproduce them.
## Secure from the first line
The core has no third-party npm dependencies, so a whole class of supply-chain risk never reaches your installation. On top of that, native input validation, authentication, signed sessions, CSRF protection and scrypt password hashing make every application secure by construction. Add the usual deployment practice of any serious project, HTTPS and a trusted proxy, and you have a solid foundation.
## Easy to learn, with a lot to start from
Total.js is JavaScript. In our experience the learning curve is very short, so a team that does not know it yet is not at a disadvantage. The Total.js repositories contain a large collection of scripts and examples, and complete applications under the MIT licence: they are ready-made solutions you can use, and clear examples of how the framework is meant to be used. Forge adds executed recipes in 14 languages to go with them.
## An ecosystem that grows with you
On the frontend, Total.js comes with jComponent and a [catalogue of hundreds of ready-made components](/en/components/), so the interface grows from the same foundations as the backend. The [official platform site](https://www.totaljs.com/) shows the wider Total.js ecosystem, with separate apps and tools for the frontend, for automation and for desktop, including tooling for Apple platforms. They build on the same foundations as the framework, and none of them is required to build a backend with Total.js.
## Evidence and scope
This guide describes capabilities of the `total5@0.0.19` package and links to executed Forge examples and official sources. The source links to `main` can change, so check the installed version before copying an API. The [execution manifest](/ai-validation.json) lists the introductory guide examples that Forge executes. Read [why choose Total.js](/en/docs/choosing-totaljs/) for the decision overview and [native assistant conventions](/en/docs/totaljs-agent-conventions/) for implementation guidance.
## Why choose Total.js for a Node.js application
Source: https://forge.totaljs.es/en/docs/choosing-totaljs/
Framework: total5@0.0.19
Content-SHA256: 808147362df4aec1a9d9eec0e5cf9621bb4e70391d2e138ea6680ddd5d9f8c41
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js 5 is a complete Node.js application framework, distributed as `total5`. One package gives a team HTTP routes, input validation, server-rendered views, authentication, uploads, WebSocket routes, scheduled jobs, localization, email and more, all designed to work together. You build the application instead of choosing, wiring and maintaining a dozen separate libraries.
## What your project gets
| What your project needs | What Total.js gives you | See it in Forge |
|---|---|---|
| An internal business application with forms, protected routes and file uploads | Native input schemas, authentication hooks and upload handling, ready to use | [Validation](/en/total5/validate-json-body/), [authentication](/en/total5/protect-routes-with-auth/), [uploads](/en/total5/file-upload/) |
| A dashboard with live updates and periodic reports | WebSocket routes, scheduled jobs and email in the same framework | [WebSocket example](/en/total5/websocket-chat/), [cron jobs](/en/total5/cron-jobs/), [email](/en/total5/send-email/) |
| A small team that wants to ship and maintain one server application | Auto-loaded files and a shared set of conventions that keep every project familiar | [Project structure](/en/docs/project-structure/), [conventions](/en/docs/totaljs-agent-conventions/) |
| A server-rendered application | Views, layouts and localization inside the same framework | [Views and layouts](/en/total5/views-and-layouts/), [localization](/en/total5/localization/) |
Every row links to an example that Forge executes. On the frontend, Total.js has jComponent and a [catalogue of hundreds of ready components](/en/components/), so the same ecosystem covers the interface too; using them is optional.
## Why teams choose Total.js
A core without external dependencies. The installed `total5` package declares no third-party runtime dependencies. There is no chain of modules written by different authors to keep compatible, no abandoned package that stops your build, no surprise from a transitive update. Continuity like this has a direct and powerful effect on how well a project scales and how easily it is maintained over the years: what you learn and ship today keeps working, backed by one project.
Built-in security APIs. The framework includes authentication, signed sessions, CSRF checks, scrypt password hashing and action input validation. Apply these facilities to your application routes and data. The [Taskboard reference application](/en/docs/taskboard-reference/) combines them in one application.
A fast learning curve. Total.js is JavaScript, with conventions that are quick to pick up, so a team that does not know it yet is not at a disadvantage. The Total.js repositories hold a large collection of scripts, examples and complete applications under the MIT licence that you can read, reuse and learn from, and Forge adds executed recipes for the rest.
Fast and light. Total.js is fast, and one package with no dependency tree keeps it light: fewer things to load, update and audit, which counts most on small servers and small devices. Forge measures it under controlled conditions and publishes latency, CPU and memory for every workload, with the raw data, in the [benchmarks](/en/benchmarks/).
## Start simple: a direct HTTP route
This executable example validates a body with a compact native schema and answers directly from a controller: a few lines and no extra layers. As the application grows, domain actions add a reusable, validated contract that you can call from routes, code and tools.
```js controllers/contacts.js
var schema = NEWSCHEMA('ContactsInput', '*name:String');
exports.install = function() {
ROUTE('GET /health/', health);
ROUTE('POST /contacts/', create);
};
function health($) {
$.json({ ok: true });
}
function create($) {
var result = schema.transform($.body);
if (result.error) {
$.response.status = 400;
$.json({ error: 'invalid' });
return;
}
$.response.status = 201;
$.json({ name: result.response.name });
}
```
## Sources
Forge pairs its claims with things you can run. The [Total.js guidelines](https://github.com/totaljs-es/guidelines) describe the ecosystem's coding style, [Total.js AI Context](https://github.com/totaljs/aicontext) supplies implementation guidance and the [source manifest](/ai-sources.json) records the reviewed revisions. If a guide and the installed framework ever differ, the installed source is the reference.
## From Express to Total.js 5
Source: https://forge.totaljs.es/en/docs/express-to-totaljs/
Framework: total5@0.0.19
Content-SHA256: 047ebe18afed21ea0bb8f52dadd4ca12918d374970fe2f9d2224b556c36c6b10
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js is not Express with another name. The biggest differences: a handler receives **one object, `$`**, instead of `(req, res)`; the body is parsed for you by its content type; you do not install packages for cookies, uploads, validation, sessions, CORS, cron or email, because they are part of the framework; and the files of your project are **loaded from folders** instead of being imported (see the project structure page).
The table pairs common Express patterns with their Total.js 5 equivalents.
| Job | Express | Total.js 5 |
|---|---|---|
| Start the server | `app.listen(3000)` | `Total.run({ port: 3000 })` |
| A route | `app.get('/users', fn)` | `ROUTE('GET /users/', fn)` (write the end slash) |
| Path parameter | `app.get('/users/:id', fn)` and `req.params.id` | `ROUTE('GET /users/{id}/', fn)` and `$.params.id` |
| Query string | `req.query.page` | `$.query.page` |
| JSON body | `app.use(express.json())` and `req.body` | `$.body`, parsed by its content type |
| Request headers | `req.headers['x-token']` | `$.headers['x-token']` |
| Send JSON | `res.json(object)` | `$.json(object)` |
| Send text or HTML | `res.send('text')` | `$.plain('text')` or `$.html('..
')` |
| Status code | `res.status(404).json(...)` | `$.invalid(404)` for an error, or `$.response.status = 201` before answering |
| Response header | `res.set('x-a', 'b')` | `$.response.headers['x-a'] = 'b'` |
| Redirect | `res.redirect('/x')` | `$.redirect('/x')` |
| Cookies | `cookie-parser`, `res.cookie(...)` | `$.cookie('name')` to read, `$.cookie('name', value, '1 day')` to write |
| Middleware on a route | `app.get('/a', mw, fn)` | `MIDDLEWARE('mw', ($, next) => ...)` and `ROUTE('GET /a/ #mw', fn)` |
| Static files | `app.use(express.static('public'))` | Put the files in `public/`: no code |
| Download a file | `res.download(path, name)` | `$.file(path, name)` |
| Template | `res.render('page', model)` | `$.view('page', model)` |
| Validate input | `express-validator` or `zod` | `input: '*email:Email, age:Number'` on an action |
| Error page | an error-handling middleware | `ROUTE('#404', fn)` and `ROUTE('#401', fn)` |
| Authentication | `passport` or `express-session` | `AUTH(fn)` and `ROUTE('+GET /private/', fn)` |
| Uploads | `multer` | the flag `@upload` and `$.files` |
| CORS | `cors` | `CORS('https://example.com')` |
| Call another API | `axios` or `fetch` | `REQUEST()` and `RESTBuilder` |
| Scheduled jobs | `node-cron` | `CRON('0 9 * * 1-5', fn)` |
| Send email | `nodemailer` | `MAIL()` and `HTMLMAIL()` with `mail_smtp` in `config` |
| WebSocket | `ws` or `socket.io` | `ROUTE('SOCKET /chat/', fn)` |
Each part has a recipe with more detail; this is the whole mapping in one place. The example below is the Total.js side of the rows that need code.
```js definitions/compare.js
MIDDLEWARE('mw', function($, next) {
$.response.headers['x-a'] = 'b';
next();
});
NEWACTION('Users/create', {
name: 'Create a user',
input: '*email:Email, age:Number',
action: function($, model) {
$.callback({ email: model.email, age: model.age });
}
});
```
```js controllers/compare.js
exports.install = function() {
ROUTE('GET /users/{id}/', user);
ROUTE('POST /users/ --> *Users/create');
ROUTE('GET /mw/ #mw', ok);
ROUTE('GET /created/', created);
ROUTE('GET /moved/', moved);
ROUTE('GET /cookie/', cookie);
ROUTE('GET /html/', html);
};
function user($) {
$.json({ id: $.params.id, page: $.query.page, token: $.headers['x-token'] });
}
function ok($) {
$.json({ ok: true });
}
function created($) {
$.response.status = 201;
$.json({ created: true });
}
function moved($) {
$.redirect('/users/1/');
}
function cookie($) {
$.cookie('visited', 'yes', '1 day');
$.json({ before: $.cookie('visited') || null });
}
function html($) {
$.html('Hello
');
}
```
## Validate a JSON body with an action
Source: https://forge.totaljs.es/en/total5/validate-json-body/
Framework: total5@0.0.19
Content-SHA256: f17fb63e9cf7f6ea86a8bf1c188191dce6312bdd0903b83f83d84e693ce5916e
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
In Total.js 5 you declare the input of an action with `input`. A star in front of a field name (`*email`) marks it as required, and the type after the colon (`Email`) is validated by the framework. Your `action` function runs only when the data is valid.
Routes point to an action with the `-->` operator. The `*` prefix means "action" (as opposed to a controller function).
```js definitions/subscribe.js
NEWACTION('Subscribe/exec', {
name: 'Subscribe to the newsletter',
input: '*email:Email, name:String',
action: function($, model) {
$.success(model.email);
}
});
```
```js controllers/subscribe.js
exports.install = function() {
ROUTE('POST /subscribe/ --> *Subscribe/exec');
};
```
Send a valid body and the action is executed. Send an invalid e-mail address and the framework answers with a validation error without calling your function.
## Expose actions through one API endpoint
Source: https://forge.totaljs.es/en/total5/api-routes/
Framework: total5@0.0.19
Content-SHA256: 27b3adc10009244d13949af7ef18f0cad861057d5c38ebc55c6cf0d8e7a6b30a
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
An API route publishes actions under one URL. The client always sends `POST` with a JSON body `{ "schema": "name", "data": { ... } }`, and Total.js dispatches it to the action registered for that name. One URL, many operations: it is the style that jComponent's `API()` function speaks.
The prefix of the name decides what happens to `data`:
- `+name`: the input of the action is required and validated
- `-name`: the action takes no input
- `%name`: partial input (for patches)
```js definitions/math.js
NEWACTION('Math/add', {
name: 'Add two numbers',
input: '*a:Number, *b:Number',
action: function($, model) {
$.callback({ sum: model.a + model.b });
}
});
NEWACTION('Math/time', {
name: 'Server time',
action: function($) {
$.callback({ year: new Date().getFullYear() });
}
});
```
```js controllers/api.js
exports.install = function() {
ROUTE('API /api/ +math_add --> Math/add');
ROUTE('API /api/ -math_time --> Math/time');
};
```
## Read URL parameters in a route
Source: https://forge.totaljs.es/en/total5/routes-with-params/
Framework: total5@0.0.19
Content-SHA256: a45ca90c0ed6335f89cecb2eedc19c0abb8191d26bbe2f487b4f2e3ff7f7b76f
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Wrap a segment of the URL in curly braces and Total.js exposes its value in `$.params`. A route can contain as many parameters as you need.
```js controllers/users.js
exports.install = function() {
ROUTE('GET /users/{id}/', show_user);
ROUTE('GET /users/{id}/posts/{post}/', show_post);
};
function show_user($) {
$.json({ id: $.params.id });
}
function show_post($) {
$.json({ id: $.params.id, post: $.params.post });
}
```
`$.json()` sends the value as JSON. Parameters are always strings; validate or convert them yourself, or declare their types in an action (see the actions recipe).
## Catch many URLs with a wildcard route
Source: https://forge.totaljs.es/en/total5/route-wildcards/
Framework: total5@0.0.19
Content-SHA256: 22d7e15bb84e561a49e3f7be0e35f2ea48aceacffdf5c94d7f215e9dce31757e
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A `*` as the last segment of a route matches any URL below that path. A route without the wildcard is more specific and wins, so you can handle the general case with the wildcard and add fixed routes for the exceptions. The request path is available in `$.url`.
```js controllers/docs.js
exports.install = function() {
ROUTE('GET /docs/*', docs);
ROUTE('GET /docs/changelog/', changelog);
};
function docs($) {
$.json({ handler: 'docs', url: $.url });
}
function changelog($) {
$.json({ handler: 'changelog' });
}
```
## Handle each HTTP method
Source: https://forge.totaljs.es/en/total5/http-methods/
Framework: total5@0.0.19
Content-SHA256: 99c1b67162ea56e00956e7570e82728a0375c0733a8a1aa59559d50cc05ce85d
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
The first word of a route is the HTTP method: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. The same URL can have a different handler for each method, and `$.method` tells a shared handler which one was used.
```js controllers/items.js
exports.install = function() {
ROUTE('GET /items/', list);
ROUTE('POST /items/', create);
ROUTE('PUT /items/', replace);
ROUTE('PATCH /items/', change);
ROUTE('DELETE /items/', remove);
};
function list($) {
$.json({ action: 'list', method: $.method });
}
function create($) {
$.json({ action: 'create', method: $.method });
}
function replace($) {
$.json({ action: 'replace', method: $.method });
}
function change($) {
$.json({ action: 'change', method: $.method });
}
function remove($) {
$.json({ action: 'remove', method: $.method });
}
```
## Restrict a route with flags
Source: https://forge.totaljs.es/en/total5/route-flags/
Framework: total5@0.0.19
Content-SHA256: 2982c373754e84082ffea5091f58ccf444ef6779ee7247e06bc3d914126b8b7c
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Flags start with `@` and go after the URL. A route only matches when the request satisfies all of its flags; otherwise Total.js tries the next route for the same URL, and answers 404 when none matches. Useful flags:
- `@json` and `@xml`: the body must be JSON or XML
- `@xhr`: the request must be an AJAX request (`x-requested-with` header)
- `@upload`: the body must be `multipart/form-data`
- `@debug` and `@release`: the route exists only in that mode
- `@mobile` and `@robot`: the client is a mobile browser or a crawler
```js controllers/data.js
exports.install = function() {
ROUTE('POST /data/ @json', json_only);
ROUTE('POST /data/ @xml', xml_only);
// Careful: a route with flags has a lower priority than the same URL without flags
ROUTE('POST /mixed/ @json', json_only);
ROUTE('POST /mixed/', anything);
};
function json_only($) {
$.json({ handler: 'json' });
}
function xml_only($) {
$.json({ handler: 'xml' });
}
function anything($) {
$.json({ handler: 'anything' });
}
```
Never mix a flagged route and an unflagged route on the same URL: the unflagged one is checked first and takes every request, so the flagged route is never reached.
## Read the query string and the request body
Source: https://forge.totaljs.es/en/total5/query-string-and-body/
Framework: total5@0.0.19
Content-SHA256: f77f4ecaf6dbba8493b0302111bc2ae2524d53427a83406f802506c9a05511f3
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Inside a route handler, `$.query` is the parsed query string and `$.body` is the parsed body. Total.js parses JSON, URL-encoded forms and multipart forms for you, based on the `content-type` of the request.
```js controllers/echo.js
exports.install = function() {
ROUTE('POST /echo/', echo);
};
function echo($) {
$.json({ query: $.query, body: $.body });
}
```
## Limit the payload size and the timeout of a route
Source: https://forge.totaljs.es/en/total5/route-limits/
Framework: total5@0.0.19
Content-SHA256: a48c8613efaaaca3952b4f51be9ecf1b7c9cc59fb01c62a256cedfec0d2d549b
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
At the end of a route you can add limits that start with `<`. `<2mb`, `<10kb` and `<500b` set the maximum size of the request body; `<30s`, `<5m` and `<1h` set the timeout of the request. A request over the limit is rejected before your handler runs.
```js controllers/limits.js
exports.install = function() {
ROUTE('POST /small/ <1kb', small);
ROUTE('POST /big/ <5mb', big);
};
function small($) {
$.json({ ok: true });
}
function big($) {
$.json({ ok: true });
}
```
## Declare typed route parameters in an action
Source: https://forge.totaljs.es/en/total5/actions-with-params/
Framework: total5@0.0.19
Content-SHA256: 4fe73fb1c01439a14bc08ee36ba7ee5d8cc02fd1d830a7477b8660e742faccf3
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
An action can describe the parameters it expects with `params`, using the same type names as `input`. UIDs are generated with `UID()` and include a checksum, so an arbitrary string is not a valid UID. When a parameter does not match its type, the framework answers with a validation error and your `action` function is not called.
```js definitions/items.js
NEWACTION('Items/read', {
name: 'Read an item',
params: 'id:UID',
action: function($) {
$.success($.params.id);
}
});
```
```js controllers/items.js
exports.install = function() {
ROUTE('GET /items/{id}/ --> *Items/read');
};
```
## Choose how to respond
Source: https://forge.totaljs.es/en/total5/responses/
Framework: total5@0.0.19
Content-SHA256: 0298973310cdc200347a02f33052883d73657d0a5e6348a855a3a8a783c2a4d7
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A handler answers the request by calling one of the response methods of `$`. Each one sets the right `content-type` for you.
| Method | Sends |
|---|---|
| `$.json(value)` | JSON |
| `$.html(string)` | HTML |
| `$.plain(string)` | plain text |
| `$.xml(string)` | XML |
| `$.redirect(url, [permanent])` | a redirect (302, or 301 when `permanent` is true) |
| `$.empty()` | an empty response (status 204) |
| `$.binary(buffer, type)` | binary data with the given content type |
```js controllers/responses.js
exports.install = function() {
ROUTE('GET /json/', ($) => $.json({ ok: true }));
ROUTE('GET /html/', ($) => $.html('Hello
'));
ROUTE('GET /plain/', ($) => $.plain('Hello'));
ROUTE('GET /xml/', ($) => $.xml('- 1
'));
ROUTE('GET /redirect/', ($) => $.redirect('/json/'));
ROUTE('GET /moved/', ($) => $.redirect('/json/', true));
ROUTE('GET /empty/', ($) => $.empty());
ROUTE('GET /binary/', ($) => $.binary(Buffer.from('abc'), 'application/octet-stream'));
};
```
## Read headers and work with cookies
Source: https://forge.totaljs.es/en/total5/headers-and-cookies/
Framework: total5@0.0.19
Content-SHA256: a51d21c0651683626fd6156136be4c9f8365400b440e26a59bf982bda65251c4
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`$.headers` holds the request headers with lowercase names. `$.cookie(name)` reads a cookie of the request and `$.cookie(name, value, expiration)` writes one to the response. Response headers are set on `$.response.headers`.
```js controllers/headers.js
exports.install = function() {
ROUTE('GET /headers/', headers);
ROUTE('GET /visit/', visit);
};
function headers($) {
$.response.headers['x-powered-by-forge'] = 'yes';
$.json({ agent: $.headers['x-agent'] || null });
}
function visit($) {
var count = ($.cookie('visits') || '0').parseInt() + 1;
$.cookie('visits', count + '', '1 day');
$.json({ visits: count });
}
```
## Receive file uploads
Source: https://forge.totaljs.es/en/total5/file-upload/
Framework: total5@0.0.19
Content-SHA256: cc8e1202c301ef7d57c6d2a4ded68c22fafe339ba7779514ef55c287d3e83d74
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Mark the route with `@upload` so it only accepts `multipart/form-data`, and set a size limit that fits the files you expect (here `<5mb`). The uploaded files are in `$.files`; the other fields of the form are in `$.body`. Each file has `name`, `filename`, `size` and `type`, and methods such as `read()`, `copy()` and `move()`.
```js controllers/upload.js
exports.install = function() {
ROUTE('POST /upload/ @upload <5mb', upload);
};
function upload($) {
var file = $.files[0];
if (!file) {
$.invalid('No file');
return;
}
file.read(function(err, buffer) {
$.json({
filename: file.filename,
size: file.size,
type: file.type,
text: buffer.toString('utf8'),
title: $.body.title
});
});
}
```
## Allow cross-origin requests with CORS
Source: https://forge.totaljs.es/en/total5/cors/
Framework: total5@0.0.19
Content-SHA256: 19d1e5e05b9e75f6423fa945f690b5f170fcdc1065d0ecbc59ba4271dd30724d
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Browsers block requests from other origins unless the server allows them. `CORS(origin)` sets the allowed origins for the whole app: pass an address, a comma-separated list, or nothing to allow every origin (`*`). Prefix an address with `+` to add it to the existing list.
```js definitions/cors.js
CORS('https://app.example.com');
```
```js controllers/api.js
exports.install = function() {
ROUTE('GET /api/time/', ($) => $.json({ ok: true }));
};
```
## Render a view without writing a handler
Source: https://forge.totaljs.es/en/total5/view-without-handler/
Framework: total5@0.0.19
Content-SHA256: 32fc1257277ade3f3462f63f6feed151aaea8fd98344211a4400a0b6e7b514a9
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
When a route has no handler, Total.js renders a view for it: `views/about.html` for `/about/` and `views/index.html` for `/`. You can also name the view yourself by passing a string instead of a function.
```html views/layout.html
@{body}
```
```html views/index.html
Home
```
```html views/about.html
About us
```
```html views/contact-page.html
Contact
```
```js controllers/default.js
exports.install = function() {
ROUTE('GET /');
ROUTE('GET /about/');
ROUTE('GET /contact/', 'contact-page');
};
```
## Pick the right input type for an action
Source: https://forge.totaljs.es/en/total5/input-types/
Framework: total5@0.0.19
Content-SHA256: 94e8b431828404458a5dd183a56f8114ec936a5e5936d5487639090ed99358f4
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
The `input` of an action is a list of `name:Type` fields separated by commas. A `*` before the name makes the field required. Besides validating, many types **convert** the value, so the action receives clean data:
| Type | Result |
|---|---|
| `String`, `Number`, `Boolean`, `Date`, `Object`, `Array` | the basic types (strings are trimmed, numbers and dates are parsed) |
| `Email`, `Phone`, `URL`, `UID`, `GUID`, `Base64`, `Color`, `Zip` | validated formats |
| `Lowercase`, `Uppercase`, `Capitalize`, `Capitalize2`, `Name` | text converted to that style: `Capitalize` capitalizes every word, `Capitalize2` only the first letter of the text and lowercases the rest |
```js definitions/types.js
NEWACTION('Types/exec', {
name: 'Typed input',
input: '*email:Email, age:Number, active:Boolean, nick:Lowercase, code:Uppercase, title:Capitalize, sentence:Capitalize2, site:URL',
action: function($, model) {
$.callback(model);
}
});
```
```js controllers/types.js
exports.install = function() {
ROUTE('POST /types/ --> *Types/exec');
};
```
## Validate the query string in an action
Source: https://forge.totaljs.es/en/total5/action-query/
Framework: total5@0.0.19
Content-SHA256: 77ad7257e53db2b3d6562b8361c9bd5d8bae8913edd81e2448d322a1898445d9
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Like `input` for the body and `params` for the path, `query` describes the query string. The framework validates it and converts the values (the page number below arrives as a real number); an invalid query answers with a validation error before your code runs.
```js definitions/search.js
NEWACTION('Search/exec', {
name: 'Search',
query: 'q:String, page:Number, *sort:Lowercase',
action: function($) {
$.callback({ q: $.query.q || '', page: $.query.page || 1, sort: $.query.sort });
}
});
```
```js controllers/search.js
exports.install = function() {
ROUTE('GET /search/ --> *Search/exec');
};
```
## Filter the response of an action with output
Source: https://forge.totaljs.es/en/total5/action-output/
Framework: total5@0.0.19
Content-SHA256: 3cc2aaaa9e55db35473103cb96c6ca1e8480d56cdbfdbf6345ba475fae365801
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`output` has the same syntax as `input`, but it is applied to the **response**: only the declared fields are sent, converted to their types. Declare it on every action that reads from a database and you cannot leak a field by accident.
```js definitions/users.js
NEWACTION('Users/read', {
name: 'Read a user',
output: 'id:String, name:String, age:Number',
action: function($) {
$.callback({ id: '1', name: 'Peter', age: '42', password: 'secret-hash', token: 'abc' });
}
});
```
```js controllers/users.js
exports.install = function() {
ROUTE('GET /user/ --> *Users/read');
};
```
## Require a user or a permission in an action
Source: https://forge.totaljs.es/en/total5/action-user-and-permissions/
Framework: total5@0.0.19
Content-SHA256: 007da97a72808f540a04b3dbd16a2c67d6178f57d549cef59c92333c78080b57
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
An action can protect itself:
- `user: true` requires an authenticated user (otherwise 401); `user: false` allows only anonymous requests
- `permissions: 'a,b'` requires the user to have at least one of the listed permissions. Total.js reads `user.permissions` (or `user.roles`), which can be an array of strings or an object
- users with `sa: true` or `su: true` (super admins) pass every check
The user comes from `AUTH()`, as in the authentication recipe.
```js definitions/auth.js
AUTH(function($) {
var users = {
'token-reports': { id: '1', name: 'Anna', permissions: ['reports'] },
'token-sales': { id: '2', name: 'Boris', permissions: ['sales'] },
'token-admin': { id: '3', name: 'Root', sa: true }
};
var user = users[$.cookie('auth')];
user ? $.success(user) : $.invalid();
});
```
```js definitions/reports.js
NEWACTION('Reports/read', {
name: 'Read reports',
user: true,
permissions: 'reports,audit',
action: function($) {
$.success($.user.name);
}
});
```
```js controllers/reports.js
exports.install = function() {
ROUTE('GET /reports/ --> *Reports/read');
};
```
## Protect routes with AUTH
Source: https://forge.totaljs.es/en/total5/protect-routes-with-auth/
Framework: total5@0.0.19
Content-SHA256: e721c95d67f27b8a74853b04e189a43be4690b3d65cd55b70462439479cb17ca
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`AUTH()` runs for every request. Call `$.success(user)` when the request is authenticated and `$.invalid()` when it is not. The user object is then available as `$.user` in every handler.
Prefix a route with `+` to allow only authenticated users and with `-` to allow only anonymous users. A route without a prefix is open to everybody.
```js definitions/auth.js
AUTH(function($) {
// This demo reads a cookie. In a real app verify a signed session instead.
var token = $.cookie('auth');
if (token === 'secret-token')
$.success({ id: '1', name: 'Peter' });
else
$.invalid();
});
```
```js controllers/private.js
exports.install = function() {
ROUTE('+GET /private/', private_page);
ROUTE('-GET /login/', login_page);
ROUTE('GET /whoami/', whoami);
};
function private_page($) {
$.json({ user: $.user.name });
}
function login_page($) {
$.json({ message: 'Please sign in' });
}
function whoami($) {
$.json({ user: $.user ? $.user.name : null });
}
```
## Declare the route inside the action
Source: https://forge.totaljs.es/en/total5/action-route-option/
Framework: total5@0.0.19
Content-SHA256: 224c93950bd1074c85e1c7f477d4a091e4a185e6f2eb3b71b467071cd1fc78a7
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Instead of a separate `ROUTE()` line, an action can carry its own route in the `route` option. The action is then the single source of truth for its URL, input and behavior.
```js definitions/ping.js
NEWACTION('Ping/exec', {
name: 'Ping',
route: 'GET /ping/',
action: function($) {
$.success('pong');
}
});
```
## Sign users in with the built-in session auth
Source: https://forge.totaljs.es/en/total5/sessions-with-built-in-auth/
Framework: total5@0.0.19
Content-SHA256: a810e4204b6a2a82ae77c2e03be8e108cd8ed1c1af85c85c4dc4bc6812c8c6ab
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Instead of a function, `AUTH(options)` accepts an options object and installs a complete cookie-session authentication:
- `secret` signs the cookie, `cookie` is its name and `expire` how long a session stays in memory without activity
- `onread(meta, next, $)` loads the user when a valid cookie arrives from a session that is not in memory (for example after a restart); call `next(null, user)`, or `next('not found')` to refuse. **Check that the session still exists in your own store here**, otherwise a copied cookie keeps working after the user signed out
- `onlogout(session)` and `onremove(sessionid)` run when a session is removed: delete it from your store
- `options.authcookie($, sessionid, userid, expiration)` signs the user in: it writes the signed cookie to the response
- `options.logout($)` removes the session and the cookie
Keep the options object in a variable: it is the handle you use to sign users in and out. After that, `$.user` is set and `+` and `-` routes work as in the AUTH recipe.
```js definitions/auth.js
var users = {
'1': { id: '1', name: 'Anna', password: 'secret' }
};
// sessionid -> userid. In a real app this is a database table.
MAIN.sessions = {};
MAIN.auth = {
secret: 'change-this-secret',
cookie: 'forge_session',
expire: '1 day',
onread: function(meta, next) {
var user = users[MAIN.sessions[meta.sessionid]];
user ? next(null, { id: user.id, name: user.name }) : next('session not found');
},
onlogout: function(session) {
delete MAIN.sessions[session.sessionid];
},
onremove: function(sessionid) {
delete MAIN.sessions[sessionid];
}
};
AUTH(MAIN.auth);
```
```js controllers/account.js
exports.install = function() {
ROUTE('POST /login/', login);
ROUTE('+POST /logout/', logout);
ROUTE('+GET /me/', me);
};
function login($) {
var user = $.body.id && $.body.id === '1' && $.body.password === 'secret' ? { id: '1', name: 'Anna' } : null;
if (!user) {
$.invalid(401);
return;
}
var sessionid = UID();
MAIN.sessions[sessionid] = user.id;
MAIN.auth.authcookie($, sessionid, user.id, '1 day');
$.json({ name: user.name });
}
function logout($) {
MAIN.auth.logout($);
$.json({ ok: true });
}
function me($) {
$.json({ name: $.user.name });
}
```
## Call an action from your own code
Source: https://forge.totaljs.es/en/total5/calling-actions-from-code/
Framework: total5@0.0.19
Content-SHA256: 12356e310300c771a563e19e888ee2e4f6f2e88d6ab607c4ee563fc5cc2ae7d1
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`ACTION(name, payload)` returns a caller that runs the action with all its validation, exactly as if it came from a route. The second argument is the payload (the input). Chain `.query()` and `.user()` to give it more data, and finish with `.callback(function(err, response) {})` or `.promise()` (use it with `await`). When the data is invalid, `err` is an error builder instead of an exception.
```js definitions/math.js
NEWACTION('Math/add', {
name: 'Add two numbers',
input: '*a:Number, *b:Number',
action: function($, model) {
$.callback({ sum: model.a + model.b });
}
});
```
```js controllers/math.js
exports.install = function() {
ROUTE('GET /sum/', sum);
ROUTE('GET /sum-promise/', sum_promise);
};
function sum($) {
ACTION('Math/add', { a: '2', b: '3' }).callback(function(err, response) {
if (err)
$.invalid(err);
else
$.json(response);
});
}
async function sum_promise($) {
try {
var response = await ACTION('Math/add', { a: 10, b: 5 }).promise();
$.json(response);
} catch (err) {
$.invalid(400);
}
}
```
## Chain several actions in one route
Source: https://forge.totaljs.es/en/total5/action-chaining/
Framework: total5@0.0.19
Content-SHA256: 8510b3a47f4f30e20391383a9d2d13e6c952729718e69a8d657359847aa55490
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A route can run more than one action: write them one after another after `-->`. They run in order, each receives the same payload, and the first error stops the chain. Add `(response)` after the action whose result must be sent; without it, the last action answers.
```js definitions/order.js
NEWACTION('Order/validate', {
name: 'Validate the order',
input: '*product:String, *quantity:Number',
action: function($, model) {
model.quantity > 0 ? $.success() : $.invalid('quantity');
}
});
NEWACTION('Order/save', {
name: 'Save the order',
input: '*product:String, *quantity:Number',
action: function($, model) {
$.callback({ id: 'order-1', product: model.product, quantity: model.quantity });
}
});
```
```js controllers/order.js
exports.install = function() {
ROUTE('POST /order/ --> *Order/validate *Order/save (response)');
};
```
## Add a middleware to a route
Source: https://forge.totaljs.es/en/total5/middleware/
Framework: total5@0.0.19
Content-SHA256: 21a8ac10bfbd464e0e9bceb13feb511303bd458fb154b488fc83b90d57a5a10c
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A middleware is a function that runs before the route handler. Call `next()` to continue or answer the request yourself to stop it. Register it once with `MIDDLEWARE(name, fn)` and attach it to a route with `#name`.
```js definitions/middleware.js
MIDDLEWARE('apikey', function($, next) {
if ($.headers['x-api-key'] === 'abc123')
next();
else
$.invalid(401);
});
```
```js controllers/secure.js
exports.install = function() {
ROUTE('GET /secure/ #apikey', secure);
ROUTE('GET /open/', open);
};
function secure($) {
$.json({ ok: true });
}
function open($) {
$.json({ ok: true });
}
```
## Build a WebSocket chat
Source: https://forge.totaljs.es/en/total5/websocket-chat/
Framework: total5@0.0.19
Content-SHA256: cf99e7bc7621eedaf3fef99ac01abd403fb18681dbe94220c553c2ec6d39bc58
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A WebSocket endpoint is declared with `ROUTE('SOCKET /path/', fn)`. Inside the handler `this` is the WebSocket controller: use `self.on('message', ...)` to receive data and `self.send(...)` to broadcast to all connected clients. `client.send(...)` answers only the sender. Objects are sent and received as JSON by default.
```js controllers/chat.js
exports.install = function() {
ROUTE('SOCKET /chat/', socket);
};
function socket() {
var self = this;
self.on('message', function(client, message) {
self.send({ type: 'message', text: message.text });
});
}
```
## Connect to a WebSocket server from Node
Source: https://forge.totaljs.es/en/total5/websocket-client/
Framework: total5@0.0.19
Content-SHA256: 4f9fb10635b7e3d5c35888a6df54fadfa832a9d2bdc4701c8b2c7c68b702e1b3
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`WEBSOCKETCLIENT(function(client) {})` gives you a client. Call `client.connect(url)` with a `ws://` or `wss://` address, listen with `client.on('open' | 'message' | 'close' | 'error', fn)` and send with `client.send(object)`; objects travel as JSON. Use `client.headers` and `client.cookies` to authenticate before connecting, and `client.close()` to disconnect.
The example connects to an echo socket of its own app; point it at any WebSocket server in real code.
```js controllers/echo.js
exports.install = function() {
ROUTE('SOCKET /echo/', echo);
ROUTE('GET /relay/', relay);
};
function echo() {
this.on('message', function(client, message) {
client.send({ echo: message.text });
});
}
function relay($) {
WEBSOCKETCLIENT(function(client) {
client.connect($.hostname('/echo/').replace('http', 'ws'));
client.on('open', function() {
client.send({ text: 'ping' });
});
client.on('message', function(message) {
client.close();
$.json(message);
});
client.on('error', function(err) {
$.invalid(err + '');
});
});
}
```
## Decouple your code with events
Source: https://forge.totaljs.es/en/total5/events/
Framework: total5@0.0.19
Content-SHA256: 8f8c320254d43e4b540edf1b8cddaf71ba605cf9f39cb70fc33445a8edfc35ee
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`ON(name, fn)` registers a listener, `EMIT(name, a, b, c...)` calls every listener with the given arguments, `ONCE(name, fn)` listens only for the first emit and `OFF(name, fn)` removes listeners. Events are in-process and synchronous. Use them to let a feature react to something (an order was paid) without the code that causes it knowing about the feature.
```js definitions/audit.js
MAIN.audit = [];
ON('order.paid', function(order) {
MAIN.audit.push('paid ' + order.id);
});
ONCE('order.first', function(order) {
MAIN.audit.push('first ' + order.id);
});
```
```js controllers/orders.js
exports.install = function() {
ROUTE('POST /pay/{id}/', pay);
ROUTE('GET /audit/', audit);
};
function pay($) {
var order = { id: $.params.id };
EMIT('order.first', order);
EMIT('order.paid', order);
$.json({ ok: true });
}
function audit($) {
$.json(MAIN.audit);
}
```
## Store and read data with the embedded NoSQL database
Source: https://forge.totaljs.es/en/total5/nosql-crud/
Framework: total5@0.0.19
Content-SHA256: 558ad712b87ce6446e701b9609e02035bc1946784e2a0f1ae6e48d9252551a10
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js ships with an embedded NoSQL database. `NOSQL('name')` opens the database file `databases/name.nosql`, creating it on first use. Operations return a builder: chain `.where()` and finish with `.callback(function(err, response) {})`.
```js definitions/notes.js
NEWACTION('Notes/insert', {
name: 'Insert a note',
input: '*text:String',
action: function($, model) {
model.id = UID();
NOSQL('notes').insert(model).callback(function() {
$.success(model.id);
});
}
});
NEWACTION('Notes/query', {
name: 'List notes',
action: function($) {
NOSQL('notes').find().callback(function(err, response) {
$.callback(response);
});
}
});
```
```js controllers/notes.js
exports.install = function() {
ROUTE('POST /notes/ --> *Notes/insert');
ROUTE('GET /notes/ --> *Notes/query');
};
```
## Query the NoSQL database
Source: https://forge.totaljs.es/en/total5/nosql-queries/
Framework: total5@0.0.19
Content-SHA256: 2b8af3412da934cdbd7d85af6450aca5066598672966481749b634c4c47d8197
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`NOSQL('name').find()` returns a query builder. Chain the conditions and finish with `.callback(function(err, response) {})`; `response` is an array of documents. The most useful methods:
- `.where('age', '>', 30)` compares a field (the operator defaults to `=`; use `>`, `<`, `>=`, `<=`, `<>`)
- `.in('city', ['Bratislava', 'Prague'])` matches any of the values
- `.between('age', 20, 40)` matches a range
- `.search('name', 'ann')` searches words inside a text field
- `.sort('age_desc')` sorts by a field (`_asc` or `_desc`)
- `.take(10)` and `.skip(20)` page the results
- `.fields('name,age')` returns only those fields
- `.first()` returns one document instead of an array
```js controllers/people.js
exports.install = function() {
ROUTE('POST /seed/', seed);
ROUTE('GET /older/', older);
ROUTE('GET /cities/', cities);
ROUTE('GET /range/', range);
ROUTE('GET /search/', search);
ROUTE('GET /page/', page);
ROUTE('GET /names/', names);
ROUTE('GET /youngest/', youngest);
};
function seed($) {
var people = [
{ id: '1', name: 'Anna Novak', age: 25, city: 'Bratislava' },
{ id: '2', name: 'Boris Kral', age: 35, city: 'Prague' },
{ id: '3', name: 'Cyril Dvorak', age: 45, city: 'Vienna' },
{ id: '4', name: 'Diana Hall', age: 30, city: 'Bratislava' }
];
people.wait(function(item, next) {
NOSQL('people').insert(item).callback(next);
}, () => $.json({ count: people.length }));
}
function older($) {
NOSQL('people').find().where('age', '>', 30).sort('age_desc').callback((err, response) => $.json(response));
}
function cities($) {
NOSQL('people').find().in('city', ['Prague', 'Vienna']).sort('name_asc').callback((err, response) => $.json(response));
}
function range($) {
NOSQL('people').find().between('age', 26, 40).sort('age_asc').callback((err, response) => $.json(response));
}
function search($) {
NOSQL('people').find().search('name', 'hall').callback((err, response) => $.json(response));
}
function page($) {
NOSQL('people').find().sort('age_asc').take(2).skip(2).callback((err, response) => $.json(response));
}
function names($) {
NOSQL('people').find().fields('name').sort('name_asc').take(2).callback((err, response) => $.json(response));
}
function youngest($) {
NOSQL('people').find().sort('age_asc').first().callback((err, response) => $.json(response));
}
```
## Update and remove NoSQL documents
Source: https://forge.totaljs.es/en/total5/nosql-update-and-remove/
Framework: total5@0.0.19
Content-SHA256: 894a7fd501ee19c762e00adca86b75172e9c997c4b994da7eb426d66e2a487d7
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`update(data)` and `remove()` work like `find()`: add the conditions with `.where()` and finish with `.callback(function(err, count) {})`. The callback receives the number of documents that were changed or removed. **Always add a condition**: without one the operation applies to every document.
`update(data)` merges the fields of `data` into the matching documents. Use `read()` to get a single document.
```js controllers/tasks.js
exports.install = function() {
ROUTE('POST /tasks/', create);
ROUTE('PATCH /tasks/{id}/', done);
ROUTE('DELETE /tasks/{id}/', remove);
ROUTE('GET /tasks/{id}/', read);
};
function create($) {
var task = { id: $.body.id, title: $.body.title, done: false };
NOSQL('tasks').insert(task).callback(() => $.json(task));
}
function done($) {
NOSQL('tasks').update({ done: true }).where('id', $.params.id).callback((err, count) => $.json({ updated: count }));
}
function remove($) {
NOSQL('tasks').remove().where('id', $.params.id).callback((err, count) => $.json({ removed: count }));
}
function read($) {
NOSQL('tasks').read().where('id', $.params.id).callback((err, response) => $.json(response || { missing: true }));
}
```
## Use the DATA API to switch databases
Source: https://forge.totaljs.es/en/total5/data-api/
Framework: total5@0.0.19
Content-SHA256: fa4d9b6515a2a695cd7f63b74ef0727aad586452f23b903a239efdd0792b4dbf
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`DATA` is a database-agnostic entry point to the query builder. The first argument is `driver/table`; `nosql/people` means "the embedded NoSQL database, table `people`". The builder methods (`where`, `sort`, `take`, `callback`) are the same you use with `NOSQL()`, so the code of your actions stays the same if you later replace the driver.
| Call | Does |
|---|---|
| `DATA.insert(table, doc)` | inserts a document |
| `DATA.find(table)` | returns a builder for a list |
| `DATA.read(table)` | returns a builder for one document |
| `DATA.update(table, data)` | updates documents |
| `DATA.remove(table)` | removes documents |
```js definitions/people.js
NEWACTION('People/insert', {
name: 'Insert a person',
input: '*name:String, age:Number',
action: function($, model) {
model.id = UID();
DATA.insert('nosql/people', model).callback(() => $.success(model.id));
}
});
NEWACTION('People/query', {
name: 'List people',
query: 'sort:Lowercase',
action: function($) {
DATA.find('nosql/people').sort($.query.sort || 'name_asc').callback((err, response) => $.callback(response));
}
});
NEWACTION('People/rename', {
name: 'Rename a person',
params: '*id:UID',
input: '*name:String',
action: function($, model) {
DATA.update('nosql/people', { name: model.name }).where('id', $.params.id).callback((err, count) => $.success(count));
}
});
```
```js controllers/people.js
exports.install = function() {
ROUTE('POST /people/ --> *People/insert');
ROUTE('GET /people/ --> *People/query');
ROUTE('PATCH /people/{id}/ --> *People/rename');
};
```
## Cache values in memory
Source: https://forge.totaljs.es/en/total5/cache/
Framework: total5@0.0.19
Content-SHA256: f39f6142602f34545a96180b8c66d6dffc02dcddad842094041e48d80b4315ea
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`CACHE` is an in-memory key-value store with expiration. `CACHE.set(key, value, expire)` stores a value (the expiration is a text such as `'10 seconds'` or `'5 minutes'`, default `'5 minutes'`). `CACHE.get(key, default)` returns the value, or the default once it has expired. `CACHE.remove(key)` deletes one key and `CACHE.reset(search)` deletes every key that contains the text.
Use it for results that are expensive to compute and may be a little stale. The cache lives in the process: it is cleared on restart and is not shared between processes.
```js controllers/cache.js
exports.install = function() {
ROUTE('GET /slow/', slow);
ROUTE('DELETE /slow/', clear);
};
var computed = 0;
function slow($) {
var cached = CACHE.get('slow-result');
if (cached) {
$.json({ value: cached, cached: true, computed: computed });
return;
}
computed++;
var value = 'result-' + computed;
CACHE.set('slow-result', value, '1 second');
$.json({ value: value, cached: false, computed: computed });
}
function clear($) {
CACHE.remove('slow-result');
$.json({ ok: true });
}
```
## Persist small settings with MEMORIZE
Source: https://forge.totaljs.es/en/total5/memorize/
Framework: total5@0.0.19
Content-SHA256: 838cecbd016addba2c097481142c15d7028bebdf4bb70ec09759c6a246cfb7ed
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`MEMORIZE(name)` returns a plain object that is saved to `databases/memorize_.json` and loaded again when the app starts. Change it with `data.set(key, value)`, which also schedules the save. Saves are delayed (10 seconds by default) so several changes become one write; pass a delay in milliseconds as the second argument to change it. It is meant for small amounts of data such as counters, flags and settings.
```js definitions/counter.js
MAIN.counter = MEMORIZE('counter', 100);
```
```js controllers/counter.js
exports.install = function() {
ROUTE('POST /hit/', hit);
ROUTE('GET /hits/', hits);
};
function hit($) {
var count = (MAIN.counter.hits || 0) + 1;
MAIN.counter.set('hits', count);
$.json({ hits: count });
}
function hits($) {
$.json({ hits: MAIN.counter.hits || 0 });
}
```
## Generate unique ids and hashes
Source: https://forge.totaljs.es/en/total5/unique-ids-and-hashes/
Framework: total5@0.0.19
Content-SHA256: a2d933d0ce2ac0a974dc8fe539259bf770d8fc8c8feedabafebf804b6285bda5
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
- `UID()` creates a short, time-ordered, unique id (12 characters). It sorts by creation time and is what the `UID` input type validates. Use it as the primary key of your documents.
- `GUID(length)` creates a random string of the requested length. Use it for tokens and file names.
- `HASH(value)` returns a fast numeric hash (not cryptographic). Use it for cache keys, ETags and sharding, never for passwords.
```js controllers/ids.js
exports.install = function() {
ROUTE('GET /ids/', ids);
};
function ids($) {
var a = UID();
var b = UID();
$.json({
uid: a,
uidlength: a.length,
valid: a.isUID(),
different: a !== b,
guid: GUID(20),
sameHash: HASH('total') === HASH('total'),
otherHash: HASH('total') !== HASH('Total'),
type: typeof(HASH('total'))
});
}
```
## Cache slow results on disk with FILECACHE
Source: https://forge.totaljs.es/en/total5/filecache/
Framework: total5@0.0.19
Content-SHA256: 199e2b942373007270d2a87c79ec2477773b3bf71f16023c0c31bf2294bcf4a9
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`FILECACHE(id, expiration, callback, maker)` looks for a cached file for `id`. If it exists and is younger than `expiration` (text such as `'1 minute'`), the callback receives its content; otherwise it calls `maker(done)` to compute the value, stores it and returns it. The result is serialized as JSON. Unlike `CACHE`, it survives restarts and works for large results.
```js controllers/report.js
exports.install = function() {
ROUTE('GET /report/', report);
};
var computed = 0;
function report($) {
FILECACHE('monthly-report', '1 minute', function(err, response) {
$.json({ report: response, computed: computed });
}, function(done) {
// The expensive work goes here
computed++;
done(null, { total: 42, generated: computed });
});
}
```
## Store uploaded files with FILESTORAGE
Source: https://forge.totaljs.es/en/total5/filestorage/
Framework: total5@0.0.19
Content-SHA256: 6cff4aa6f8c4f6223f79ebf823911d6bce9231c2d2cab8e006db84ea9817ae7b
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`FILESTORAGE(name)` is a file store: files are saved under an id (use `UID()`) and served back by that id. Receive the upload with `@upload`, save it with `file.fs(storage, id, callback)` and answer downloads with `$.filefs(storage, id)`, which sends the stored file with its `content-type`.
The storage name comes from the config file, so it can change per environment.
```text config
fs : files
```
```js controllers/files.js
exports.install = function() {
ROUTE('POST /upload/ @upload <5mb', upload);
ROUTE('FILE /downloads/*.*', download);
};
function upload($) {
var file = $.files[0];
if (!file) {
$.invalid('No file');
return;
}
var id = UID();
file.fs(CONF.fs, id, function(err) {
if (err)
$.invalid(err);
else
$.json({ id: id, url: '/downloads/' + id + '.' + file.extension, filename: file.filename, size: file.size });
});
}
function download($) {
// /downloads/.
var name = $.split[1];
var id = name.substring(0, name.lastIndexOf('.'));
$.filefs(CONF.fs, id);
}
```
## Render a view with a layout
Source: https://forge.totaljs.es/en/total5/views-and-layouts/
Framework: total5@0.0.19
Content-SHA256: 3f9754de17a1ccea82afe76a0a86a5df3fbd28ef19fa8d9d1d2b04559ee301c1
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`$.view(name, model)` renders `views/.html` and wraps it in `views/layout.html`. Inside a view the data is available as `model`; use `@{...}` for expressions, `@{foreach ... in ...}` to loop and `@{body}` in the layout to place the view.
The layout is rendered without the model. To give it values (a page title, for example) use `repository`: the object returned by `$.view()` exposes it.
```html views/layout.html
@{repository.title}
@{body}
```
```html views/index.html
@{model.name}
@{foreach item in model.items}
- @{item}
@{end}
```
```js controllers/default.js
exports.install = function() {
ROUTE('GET /', home);
};
function home($) {
var view = $.view('index', { name: 'Forge', items: ['routes', 'actions'] });
view.repository.title = 'Home';
}
```
## Use conditions and escaping in views
Source: https://forge.totaljs.es/en/total5/view-conditions-and-escaping/
Framework: total5@0.0.19
Content-SHA256: 90896da172ea4470dd44d3030fece11a2f2328706f1f1a43bb7c6536bf95f7e2
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Views are HTML with `@{...}` blocks. A condition is `@{if expression}`, optionally `@{else}`, and always closed with `@{fi}`. The expression is plain JavaScript and can use `model` (the data passed to `$.view()`), `repository` (values shared with the layout), `query`, `user`, `config` and `language`.
Output is **escaped by default**: `@{model.text}` turns `<` into `<`, which protects you from injection. Put a `!` after `@{` to send the raw value (`@{!model.html}`) and do it only with content you trust.
```html views/index.html
@{layout('')}
@{if model.admin}
Administrator
@{else}
Guest
@{fi}
@{if model.items.length > 2}
many items
@{fi}
@{model.text}
@{!model.text}
```
```js controllers/default.js
exports.install = function() {
ROUTE('GET /{role}/', page);
};
function page($) {
$.view('index', { admin: $.params.role === 'admin', items: [1, 2, 3], text: 'Hello World' });
}
```
## Loop over arrays and objects in views
Source: https://forge.totaljs.es/en/total5/view-loops/
Framework: total5@0.0.19
Content-SHA256: adab60f6b0585cd9ac35414a22c15ca854687debd2d504904f69cd77d07e4aff
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`@{foreach item in expression}` repeats the block for every element of an array, closing with `@{end}`. Inside the loop `index` is the position of the current element (starting at 0). The same syntax iterates the properties of an object: every element has a `key` and a `value`.
```html views/index.html
@{layout('')}
@{foreach product in model.products}
- @{index}: @{product.name} - @{product.price}
@{end}
@{foreach field in model.meta}
- @{field.key}
- @{field.value}
@{end}
```
```js controllers/default.js
exports.install = function() {
ROUTE('GET /', page);
};
function page($) {
$.view('index', {
products: [{ name: 'Book', price: 12 }, { name: 'Pen', price: 3 }, { name: 'Lamp', price: 40 }],
meta: { color: 'red', size: 'M' }
});
}
```
## Include views and assets
Source: https://forge.totaljs.es/en/total5/view-includes-and-assets/
Framework: total5@0.0.19
Content-SHA256: b4ee4be42f5d663ec94a6951b8a6f6ee0f7e19eb6fa93a8b0274e4cbfbee6246
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`@{view(name, model)}` renders another view in place, so repeated pieces (a card, a row, a footer) live in one file. The second argument is the model of the included view.
`@{import('a.js', 'a.css')}` writes the `
Invoice for {{ value.customer }}
Total: {{ value.total | money() }}
```
```js controllers/documents.js
exports.install = function() {
ROUTE('GET /text/', text);
ROUTE('GET /file/', file);
ROUTE('GET /default/', withdefault);
ROUTE('GET /absolute/', absolute);
ROUTE('GET /alias/', alias);
};
async function text($) {
$.plain(await TEMPLATE('Hello {{ value.name }}!', { name: $.query.name || 'world' }));
}
async function file($) {
$.plain(await TEMPLATE('invoice', { customer: 'Ana', total: 12.5 }));
}
function alias($) {
$.json({ same: Ta === Tangular, rendered: Ta.render('{{ name }}', { name: 'x' }) });
}
async function absolute($) {
const file = require('path').join(__dirname, '..', 'templates', 'invoice.html');
$.plain(await TEMPLATE('~' + file, { customer: 'Eva', total: 1 }));
}
async function withdefault($) {
$.plain(await TEMPLATE('invoice'));
}
```
## Compare two lists with DIFFARR: what to add, update and remove
Source: https://forge.totaljs.es/en/total5/diffarr-sync-lists/
Framework: total5@0.0.19
Content-SHA256: a12236697023df4617897c5ec8a240897c9b3ae8fe6c8ec55b1717c1d1f9bdaf
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`DIFFARR(property, saved, submitted)` compares two arrays of objects by one property and returns an object with three lists:
- `add`: the objects of `submitted` whose property is **not** in `saved`
- `upd`: for the objects that are in both, `{ db, form }` with the saved object and the submitted one
- `rem`: the **values of the property** (not the objects) of the items of `saved` that are not in `submitted`
It is the usual step of saving an editable list: the form sends the whole list and you need to know what was created, what changed and what was deleted. The values are compared with `==`, so `1` and `'1'` are the same item.
```js controllers/lists.js
const saved = [
{ id: 1, name: 'Book' },
{ id: 2, name: 'Pen' },
{ id: 3, name: 'Lamp' }
];
exports.install = function() {
ROUTE('POST /sync/', sync);
};
function sync($) {
const diff = DIFFARR('id', saved, $.body.items);
$.json({
add: diff.add,
update: diff.upd.map(n => ({ id: n.db.id, from: n.db.name, to: n.form.name })),
remove: diff.rem
});
}
```
## Query builder: DB() and NEWDB() with your own database driver
Source: https://forge.totaljs.es/en/total5/query-builder-db/
Framework: total5@0.0.19
Content-SHA256: 4bef065e707876cf8653fcd1df4979bebd7327aa58465f355fefd40b2d4ca631
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`DB()` builds a query without knowing which database will run it, and `NEWDB()` registers the **driver** that runs it. `total5` itself has no SQL driver: the query builder collects what you ask for into an `options` object and calls your driver with it, and the driver (yours, or one that comes in a module) translates `options` into SQL, a NoSQL call or anything else.
- `NEWDB(driver)` registers the default driver and `NEWDB('name', driver)` a named one. The driver is `function(options, callback)`: answer with `callback(error, response)`. A table written as `name/table` (`DB().find('audit/log')`) uses the driver `name` and the table `log`; without a prefix the driver is `default`.
- The operations start the query: `find(table)` (an array), `read(table)` (one object, or `null` when there is none), `list(table)` (a page of an array: `response.page`, `limit` and `pages` are added to what the driver returns, so the driver should set `count`) and `insert(table, data)`. The builder also has `count`, `update`, `remove` and `query`; they work the same way (the driver gets `options.exec` with that name).
- Refine the query with: `where(name, [comparer], value)` (`=` when there is no comparer), `fields('a,b')`, `sort(name, [true])` (`true` for descending), `take(n)` and `page(page, limit)`. Other modifiers include `id`, `in`, `skip`, `between` and `search`.
- Finish with `.callback($)`: the result goes to the response of the action `$`, or to a function `(err, response)` that you pass instead.
For the complete native operation catalogue, upsert/error behavior and filter shapes, see [query-builder-contract](/en/total5/query-builder-contract/). For SQL CRUD, commit and rollback, see [query-builder-sqlite](/en/total5/query-builder-sqlite/). The options the driver receives are plain data. The example driver stores records in memory and exposes the last `options` object received.
```js definitions/db.js
const users = [
{ id: 1, name: 'Ann', age: 31 },
{ id: 2, name: 'Bob', age: 17 },
{ id: 3, name: 'Cy', age: 45 }
];
F.seen = [];
// The default driver: it understands only "age >= value" and "id = value", which is all this example asks
NEWDB(function(options, callback) {
F.seen.push(JSON.parse(JSON.stringify(options)));
let rows = users.slice();
for (let f of options.filter) {
if (f.type === 'where' && f.name === 'age' && f.comparer === '>=')
rows = rows.filter(n => n.age >= f.value);
else if (f.type === 'where' && f.name === 'id')
rows = rows.filter(n => n.id === f.value);
}
switch (options.exec) {
case 'find':
case 'read':
if (options.sort && options.sort[0] === 'name_desc')
rows.sort((a, b) => a.name < b.name ? 1 : -1);
if (options.take)
rows = rows.slice(0, options.take);
return callback(null, rows);
case 'list':
rows.count = rows.length;
return callback(null, rows);
case 'insert':
users.push(Object.assign({ id: users.length + 1 }, options.payload));
return callback(null, users[users.length - 1]);
default:
return callback('The driver does not know ' + options.exec);
}
});
// A second driver, used by the tables that start with "audit/"
NEWDB('audit', function(options, callback) {
F.seen.push({ driver: 'audit', table: options.table });
callback(null, [{ event: 'login' }]);
});
```
```js controllers/users.js
exports.install = function() {
ROUTE('GET /adults/', adults);
ROUTE('GET /user/{id}/', user);
ROUTE('POST /users/', create);
ROUTE('GET /page/', page);
ROUTE('GET /audit/', audit);
ROUTE('GET /seen/', seen);
};
function adults($) {
DB().find('users').where('age', '>=', 18).fields('id,name').sort('name', true).take(5).callback($);
}
function user($) {
DB().read('users').where('id', +$.params.id).callback($);
}
function create($) {
DB().insert('users', { name: $.body.name, age: $.body.age }).callback($);
}
function page($) {
DB().list('users').page(1, 2).callback(function(err, response) {
$.json({ err: err, count: response.count, page: response.page, limit: response.limit, pages: response.pages });
});
}
function audit($) {
DB().find('audit/log').callback($);
}
function seen($) {
$.json(F.seen);
}
```
## Configure the app with the config file
Source: https://forge.totaljs.es/en/total5/configuration-file/
Framework: total5@0.0.19
Content-SHA256: bd1905565ada99b1bad4e9ad56a8a762ec12643f80e1058a0b2fd82f985c920b
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
The `config` file in the root of the project holds one `key : value` setting per line. At runtime the values are available in the global `CONF` object. Values are strings unless you declare a type in brackets after the key: `limit (number) : 10` or `debug (boolean) : true`.
```text config
name : Forge demo
limit (number) : 10
debug (boolean) : true
```
```js controllers/settings.js
exports.install = function() {
ROUTE('GET /settings/', settings);
};
function settings($) {
$.json({ name: CONF.name, limit: CONF.limit, debug: CONF.debug });
}
```
In a view you can read the same values with `@{'%name'}`.
## Encrypt and decrypt data with a key
Source: https://forge.totaljs.es/en/total5/encrypt-and-decrypt/
Framework: total5@0.0.19
Content-SHA256: 304fb571fa9f2b8a3e29e7adac76abcaedffa2c5b3c1cfb2b34d5cc169dc4be1
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`ENCRYPT(value, key)` serializes a value to JSON and returns an opaque text. `DECRYPT(text, key)` returns the original value, or `null` when the text was changed, is not valid, or was encrypted with another key. The `secret` setting of your config is mixed into the key, so tokens made on one installation do not open on another.
Use it for password reset links, invitation tokens and anything you need to hand to a client and verify later without storing it. Put an expiration inside the value (a timestamp) and check it after decrypting.
```text config
secret : change-this-secret-in-production
```
```js controllers/tokens.js
exports.install = function() {
ROUTE('GET /token/', create);
ROUTE('GET /verify/', verify);
};
function create($) {
var token = ENCRYPT({ userid: '123', expire: Date.now() + 60000 }, 'reset-password');
$.json({ token: token });
}
function verify($) {
var data = DECRYPT($.query.token || '', 'reset-password');
if (!data || data.expire < Date.now()) {
$.invalid(401);
return;
}
$.json({ userid: data.userid });
}
```
## Call external services with API() and NEWAPI() drivers
Source: https://forge.totaljs.es/en/total5/api-drivers/
Framework: total5@0.0.19
Content-SHA256: a619fa8d5764560b83f75ce806ad4bb4214635ebaaf86d1ae315dd25a11c21bf
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`API(name, schema, data, [$])` calls an operation of an external service and `NEWAPI()` registers the **driver** that makes the call, so the code of your actions does not depend on how the service is reached (HTTP, a library, a queue...).
- `NEWAPI('name', driver)` registers the driver of the service `name` (`'a,b'` registers one driver for two names, and `NEWAPI(driver)` registers the `default` one). `NEWAPI('name', config, driver)` also stores a `config` object that every call receives as `options.config`.
- The driver is `function(options, next)`. `options.name` is the service, `options.schema` the operation, `options.data` the data and `options.config` the configuration. Answer with `next(error, response)`.
- `API('name', 'operation', data)` returns a builder: finish it with `.callback($)` (the response goes to the action `$`), `.callback(function(err, response) {})` or `await ....promise($)`. `.configure({ key: value })` adds to `options.config` for that call, and `.error('text')` makes an empty answer fail with that message.
- If no driver is registered for the name the call fails with `API is not initialized`.
```js definitions/api.js
// The driver of a fake weather service: in a real one this would be an HTTP request
NEWAPI('weather', { unit: 'C' }, function(options, next) {
if (options.schema === 'forecast')
next(null, { city: options.data.city, temperature: 21, unit: options.config.unit, service: options.name });
else
next('Unknown operation ' + options.schema);
});
```
```js controllers/weather.js
exports.install = function() {
ROUTE('GET /forecast/', forecast);
ROUTE('GET /forecast-fahrenheit/', fahrenheit);
ROUTE('GET /unknown/', unknown);
ROUTE('GET /missing/', missing);
ROUTE('GET /promise/', promise);
};
function forecast($) {
API('weather', 'forecast', { city: $.query.city }).callback($);
}
function fahrenheit($) {
API('weather', 'forecast', { city: $.query.city }).configure({ unit: 'F' }).callback($);
}
function unknown($) {
API('weather', 'rain', {}).callback($);
}
function missing($) {
API('nobody', 'forecast', {}).callback($);
}
async function promise($) {
const data = await API('weather', 'forecast', { city: 'Oslo' }).promise($);
$.json({ city: data.city, same: true });
}
```
## Talk to language models with AIMODEL(), NEWAIMODEL() and AIPARSER()
Source: https://forge.totaljs.es/en/total5/ai-models/
Framework: total5@0.0.19
Content-SHA256: 7c29008f55a6bdf6dc0904f945e8272bb1a3a32c18781f7f4039c6da23dfb8e9
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`total5` does not ship a connection to any provider. It gives you three pieces that work together:
- `NEWAIMODEL('name', config, driver)` registers the **driver** of one or more models (`'a,b'` registers the same driver for two names). The driver is `function(ai, done)`: `ai.payload` has `model`, `messages`, `tools`, `prompt` and `think`, `ai.config` is the configuration and `ai.options.stream` is the function the caller passed to `stream()` (or `undefined`). Answer with `done(error, response)`.
- `AIMODEL('name')` starts a request for a registered model and returns a builder. `system(text)`, `user(text)` and `assistant(text)` append a message, `prompt(text)`, `tool(definition)` and `think()` fill the rest of the payload, and `configure({ ... })` adds to `ai.config` for this call only. Finish it with `callback(fn)`, `await promise($)` or `stream(fn)`; the driver runs after the current turn, so you can chain everything first.
- `AIPARSER(provider)` returns a parser for the **streamed** answer of a provider (the source of `total5` knows `openai`, `claude`/`anthropic`, `gemini`/`google`, `ollama` and `openai_responses`, and reads any other name as a generic format; only `openai` is run below). `write(chunk)` takes pieces of the stream (it keeps incomplete lines until the next piece), `end()` finishes it, and both return `{ content, thinking, tools, reasoning }`. `parser.on('content', fn)` is called with every part of the text as it arrives.
A model that was not registered answers with the error `AI model not found.`. The example registers a local driver that returns a response from the supplied messages.
```js definitions/ai.js
NEWAIMODEL('echo', { prefix: 'Echo' }, function(ai, done) {
const last = ai.payload.messages.filter(n => n.role === 'user').pop();
const text = ai.config.prefix + ': ' + (last ? last.content : '');
// A real driver would send ai.payload to the provider and send the pieces to ai.options.stream
if (ai.options.stream) {
for (const word of text.split(' '))
ai.options.stream(word + ' ');
}
done(null, { content: text, roles: ai.payload.messages.map(n => n.role), tools: ai.payload.tools.length, think: !!ai.payload.think });
});
```
```js controllers/ai.js
exports.install = function() {
ROUTE('POST /ask/', ask);
ROUTE('GET /ask-config/', askconfig);
ROUTE('GET /ask-stream/', askstream);
ROUTE('GET /ask-missing/', missing);
ROUTE('GET /parse/', parse);
};
async function ask($) {
const response = await AIMODEL('echo').system('You are brief.').user($.body.question).tool({ name: 'weather' }).think().promise($);
$.json(response);
}
function askconfig($) {
AIMODEL('echo').user('hello').configure({ prefix: 'Custom' }).callback($.callback());
}
function askstream($) {
const parts = [];
AIMODEL('echo').user('one two three').stream(part => parts.push(part)).callback(function(err, response) {
$.json({ parts: parts, content: response.content });
});
}
function missing($) {
AIMODEL('nobody').user('hi').callback($.callback());
}
function parse($) {
const parser = AIPARSER('openai');
const events = [];
parser.on('content', e => events.push(e.delta));
// the second piece starts in the middle of a line
parser.write('data: {"choices":[{"delta":{"content":"Hel"}}]}\n\ndata: {"choices":[{"delta":{"con');
parser.write('tent":"lo"}}]}\n\ndata: [DONE]\n');
const output = parser.end();
$.json({ output: output, events: events });
}
```
## Limit failed attempts with BLOCKED
Source: https://forge.totaljs.es/en/total5/rate-limiting/
Framework: total5@0.0.19
Content-SHA256: 7e8fac3c4d653cf0d6f7b29e6b60cac123f37a0805deb8184eb6ac990cb10230
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`BLOCKED($, limit, expiration)` counts calls per IP address. It returns `true` once the address has made more than `limit` calls within the expiration (default: 5 calls in 15 minutes), and `undefined` before that. Call it at the start of a sensitive handler and stop when it returns `true`. To forgive an address after a successful login call `BLOCKED($, -1)`.
The counters live in the memory of the process; behind a reverse proxy make sure the real client address reaches Total.js, otherwise everybody shares one counter.
```js controllers/login.js
exports.install = function() {
ROUTE('POST /login/', login);
};
function login($) {
if (BLOCKED($, 3, '1 minute')) {
$.invalid(429);
return;
}
if ($.body.password === 'correct') {
BLOCKED($, -1);
$.json({ ok: true });
} else
$.invalid(401);
}
```
## The small global helpers: MAIN, REPO, FUNC, NOW, CLONE, COPY, QUERIFY and PROMISIFY
Source: https://forge.totaljs.es/en/total5/utility-globals/
Framework: total5@0.0.19
Content-SHA256: fe1db1d0b9705cf0ab8ab5aae2d10d62c6f0ca3c79286a1c96bcb884b0dc7f02
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`total5` creates a few global objects for your own use and a few helpers that save a line of code. Nothing here needs setup.
- `MAIN`, `REPO` and `FUNC` are empty objects that the framework creates once: keep application data in `MAIN`, other data in `REPO` and your own functions in `FUNC`. `TEMP` is an object for temporary data that the framework replaces from time to time, so do not keep anything in it that you need later.
- `NOW` is the current date as a `Date`. It is refreshed about once a minute (and by some calls), so use `new Date()` when you need the exact time.
- `EMPTYARRAY` and `EMPTYOBJECT` are frozen empty values to return instead of creating new ones: they cannot be changed.
- `MODS` and `PLUGINS` are objects with the loaded modules and plugins by id. `F` and `Total` are the same object, the framework, and `print` is `console.log`. `NOOP` is a function that does nothing.
- `REQUIRE(name)` is `require` that also resolves packages from the `node_modules` of the application and accepts names such as `node:path`.
- `CLONE(value)` makes a deep copy: nested objects, arrays and dates are copied. `COPY(from, to)` copies the properties of the first object into the second and returns the second.
- `QUERIFY([url], object)` builds a query string from an object: empty values and `null` are skipped and an array becomes a comma separated value. With an url it returns `url?...`, without one it returns `?...`.
- `PROMISIFY(fn, a, b, c)` calls `fn(a, b, c, callback)` and returns a promise that resolves with the response of the callback or rejects with its error.
```js controllers/helpers.js
MAIN.started = 'yes';
REPO.counter = 1;
FUNC.double = n => n * 2;
exports.install = function() {
ROUTE('GET /helpers/', helpers);
ROUTE('GET /promisify/', promisify);
};
function helpers($) {
const source = { a: 1, nested: { b: [1, 2] }, date: new Date(2026, 0, 5) };
const cloned = CLONE(source);
const target = { x: 1 };
const returned = COPY({ y: 2 }, target);
$.json({
shared: [MAIN.started, REPO.counter, FUNC.double(4), typeof TEMP],
now: [NOW instanceof Date, Math.abs(Date.now() - NOW.getTime()) < 120000],
emptyarray: [EMPTYARRAY.length, Object.isFrozen(EMPTYARRAY)],
emptyobject: [Object.keys(EMPTYOBJECT).length, Object.isFrozen(EMPTYOBJECT)],
registries: [typeof MODS, typeof PLUGINS],
same: [F === Total, typeof print, NOOP() === undefined],
require: typeof REQUIRE('node:path').join,
clone: [cloned !== source, cloned.nested !== source.nested, cloned.nested.b !== source.nested.b, cloned.date instanceof Date, cloned.nested.b.join()],
copy: [returned === target, target.x, target.y],
querify: QUERIFY('/search/', { q: 'a b', page: 2, empty: '', nothing: null, list: [1, 2] }),
querify2: QUERIFY({ q: 'x' })
});
}
async function promisify($) {
const add = (a, b, callback) => setImmediate(callback, null, a + b);
const fail = (callback) => setImmediate(callback, 'It failed');
const sum = await PROMISIFY(add, 2, 3);
let error = null;
try {
await PROMISIFY(fail);
} catch (e) {
error = e.message || String(e);
}
$.json({ sum: sum, error: error });
}
```
## Collect errors with ErrorBuilder, log them with ERROR() and keep an audit trail with AUDIT()
Source: https://forge.totaljs.es/en/total5/errors-and-audit/
Framework: total5@0.0.19
Content-SHA256: ccd28753dcc2801ab0c0c92f984123b5941dfcfca8fdb41cbdcbf591eba8a958
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
- **`ErrorBuilder`** collects errors. `push(message, [path], [index])` adds one (`path` is the field it belongs to), `push(404)` adds the text of a status code and sets the status of the answer, `length` counts them, `output()` returns them as an array of `{ error, path, index }` and `toString()` joins the messages. `$.invalid(builder)` answers with that array and the status of the builder (400 unless a status code was pushed).
- **`SUCCESS(value)`** returns the object for a successful operation: `{ success: true, value: value }`.
- **`ERROR(name)`** returns a callback for `function(err)` APIs: when it receives an error it records it under `name`, and it does nothing when the error is empty. `ERROR()` is the same callback without a name. A recorded error is printed to the console, counted in `F.stats.error`, and the last ten are kept in `F.errors` as `{ error, name, date }`.
- **`AUDIT($, message, type)`** appends one JSON line to `logs/audit.log` with who and where (`ip`, `ua`, `url`, and the user when there is one), the `message`, the `type`, the `params`, `query` and `model` of the request in `data`, the name of the application in `app` and the time in `dtcreated`. `AUDIT('name', $, message, type)` writes to `logs/name.log` instead.
```js controllers/reports.js
exports.install = function() {
ROUTE('GET /builder/', builder);
ROUTE('GET /builder404/', builder404);
ROUTE('GET /success/', success);
ROUTE('GET /error/', error);
ROUTE('GET /audit/', audit);
ROUTE('GET /audit-read/{name}/', auditread);
};
function builder($) {
const errors = new ErrorBuilder();
errors.push('Name is required', 'name');
errors.push('Age is too low', 'age', 2);
$.invalid(errors);
}
function builder404($) {
const errors = new ErrorBuilder();
errors.push(404);
$.invalid(errors);
}
function success($) {
$.json(SUCCESS('saved'));
}
function error($) {
const before = F.stats.error;
ERROR('mypart')('Something broke');
ERROR('mypart')(null);
ERROR()('Without a name');
$.json({ counted: F.stats.error - before, last: F.errors.slice(-2).map(n => ({ error: n.error, name: n.name || null, date: n.date instanceof Date || typeof n.date === 'string' })) });
}
function audit($) {
AUDIT($, 'Opened the report', 'info');
AUDIT('security', $, 'Checked the access');
$.json({ ok: true });
}
function auditread($) {
setTimeout(function() {
const lines = require('fs').readFileSync(PATH.logs($.params.name + '.log'), 'utf8').trim().split('\n');
$.json(lines.map(n => JSON.parse(n)));
}, 300);
}
```
## Debounce work with setTimeout2() and clearTimeout2()
Source: https://forge.totaljs.es/en/total5/debounce-timers/
Framework: total5@0.0.19
Content-SHA256: e0a20f3b90203a51ae4c6bbdfe81c1fc468b6bba946d9f8046b8b7ad35a02bac
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`setTimeout2(id, callback, timeout)` is `setTimeout` with a name. When you call it again with the **same id** before the time is over, the previous timer is cancelled and replaced: the callback runs once, after the calls stop. It is the usual way to save, reload or recalculate only once when many events arrive together (a burst of changes, typing, a flood of messages).
`clearTimeout2(id)` cancels the timer of that id and returns `true` when there was one and `false` when there was not.
The full form is `setTimeout2(id, callback, timeout, limit, a, b, c, d, e)`: the values after `limit` are passed to the callback. **Do not use `limit`** in `total5` 0.0.19: the idea is to run the callback at once after `limit` replacements, but the counter never starts (it is `undefined` and becomes `NaN`), so the callback is only ever called when the time is over. The example calls it ten times with a limit of 3 and the callback did not run early.
```js controllers/timers.js
exports.install = function() {
ROUTE('GET /burst/', burst);
};
async function burst($) {
const log = [];
// three calls with the same id: only the last one runs
setTimeout2('save', () => log.push('first'), 100);
setTimeout2('save', () => log.push('second'), 100);
setTimeout2('save', () => log.push('third'), 100);
// a timer that is cancelled before it runs
setTimeout2('gone', () => log.push('gone'), 100);
const cancelled = clearTimeout2('gone');
const nothing = clearTimeout2('never-set');
// the values after the limit reach the callback
setTimeout2('args', (a, b) => log.push(a + b), 50, 0, 'x', 'y');
// the limit option does not run the callback early
let early = 0;
for (let i = 0; i < 10; i++)
setTimeout2('limited', () => early++, 1000, 3);
const stopped = clearTimeout2('limited');
await new Promise(resolve => setTimeout(resolve, 300));
$.json({ log: log, cancelled: cancelled, nothing: nothing, early: early, stopped: stopped });
}
```
## Tokens tied to a browser with ENCRYPTREQ() and DECRYPTREQ()
Source: https://forge.totaljs.es/en/total5/request-bound-tokens/
Framework: total5@0.0.19
Content-SHA256: 8cdff67bb59806a632b3b63151bc64dc8416ae5d8892adb0e9296bb52e288e0a
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`ENCRYPTREQ($, value, key, [strict])` encrypts a value **together with the user agent of the request** and returns a text token. `DECRYPTREQ($, token, key)` reads the value back, and returns nothing (`undefined`) when the key is wrong, the token is empty, or the request comes from a different client. With `strict` set to `true` the IP address of the request is part of the token too, so it also stops working when the address changes.
Use it for values that you hand to the browser and expect back from the same browser: a short-lived link, a download address, a form token. It is not a replacement for a session: the user agent is easy to copy. For tokens that do not depend on the request see the encrypt and decrypt recipe.
```js controllers/tokens.js
exports.install = function() {
ROUTE('GET /issue/', issue);
ROUTE('GET /read/', read);
ROUTE('GET /read-wrong-key/', wrongkey);
};
function issue($) {
$.json({ token: ENCRYPTREQ($, { id: 5 }, 'my-secret'), strict: ENCRYPTREQ($, 'only-here', 'my-secret', true) });
}
function read($) {
$.json({ value: DECRYPTREQ($, $.query.token, 'my-secret') || null });
}
function wrongkey($) {
$.json({ value: DECRYPTREQ($, $.query.token, 'another-secret') || null });
}
```
## Back up a folder with BACKUP() and RESTORE(), and run commands with SHELL()
Source: https://forge.totaljs.es/en/total5/backup-restore-shell/
Framework: total5@0.0.19
Content-SHA256: 951ae399addf9763c314144b8f259adde20d5bd8d856698108de9e75dce43fde
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
- `BACKUP(filename, directory, [callback], [filter])` packs a directory (with its subdirectories) into **one file** and resolves with `{ filename, files, size }`. An existing file with that name is replaced. Without a callback it returns a promise.
- `RESTORE(filename, directory, [callback], [filter])` unpacks a file made with `BACKUP()` into a directory, creating the folders it needs, and resolves with `{ files, size, path }`.
- `SHELL(command, [callback], [cwd])` runs a shell command and resolves with its output (`stdout`). The folder is the one of the project, or the text you pass as the last argument. When the command fails the promise is rejected with an error whose `code` is the exit code. With a callback it is called as `callback(err, stdout, stderr)`.
`SHELL()` runs **exactly the text you give it**: never build the command from what a user sent.
```js controllers/backups.js
const Fs = require('fs');
const Path = require('path');
exports.install = function() {
ROUTE('GET /backup/', backup);
ROUTE('GET /shell/', shell);
};
async function backup($) {
// a folder with a file and a subfolder
const source = PATH.tmp('source');
Fs.rmSync(source, { recursive: true, force: true });
Fs.mkdirSync(Path.join(source, 'sub'), { recursive: true });
Fs.writeFileSync(Path.join(source, 'a.txt'), 'hello');
Fs.writeFileSync(Path.join(source, 'sub', 'b.txt'), 'world');
const file = PATH.tmp('folder.backup');
const packed = await BACKUP(file, source);
const target = PATH.tmp('restored');
Fs.rmSync(target, { recursive: true, force: true });
const unpacked = await RESTORE(file, target);
$.json({
packed: [packed.files, packed.filename === file, Fs.existsSync(file)],
unpacked: [unpacked.files, unpacked.size, unpacked.path === target],
content: [Fs.readFileSync(Path.join(target, 'a.txt'), 'utf8'), Fs.readFileSync(Path.join(target, 'sub', 'b.txt'), 'utf8')]
});
}
async function shell($) {
const output = await SHELL('echo hello');
const where = await SHELL('pwd', PATH.tmp());
let code = null;
try {
await SHELL('exit 3');
} catch (e) {
code = e.code;
}
SHELL('echo from callback', (err, stdout) => $.json({ output: output, where: where.trim() === PATH.tmp() || where.trim().endsWith('/tmp'), code: code, callback: stdout }));
}
```
## Put the application in maintenance with PAUSESERVER()
Source: https://forge.totaljs.es/en/total5/pause-server/
Framework: total5@0.0.19
Content-SHA256: 9906e044fb40f45c1560ab417e7d4840431f15fe4159546344939e06b8075510
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`PAUSESERVER(name, true)` pauses the server and `PAUSESERVER(name, false)` resumes it. While **at least one name** is paused, every request is answered with the status **503** and the built-in maintenance page (a request made with `xhr` gets a 503 error answer instead of the page). The name only identifies who paused it, so that two parts of the application can pause and resume independently: the server answers again when all the names were resumed.
Without the second argument the call **toggles**: `PAUSESERVER(name)` pauses when the name was not paused and resumes when it was, and returns the new state (`true` when paused).
Because a paused server answers 503 to everything, including the route that would resume it, resume from your own code: a timer, a signal, a task that finishes. The example pauses for a short time and resumes by itself.
```js controllers/maintenance.js
exports.install = function() {
ROUTE('GET /maintenance/', maintenance);
ROUTE('GET /toggle/', toggle);
ROUTE('GET /ping/', ping);
};
function maintenance($) {
const state = PAUSESERVER('maintenance', true);
setTimeout(() => PAUSESERVER('maintenance', false), 600);
$.json({ paused: state });
}
function toggle($) {
const first = PAUSESERVER('toggled');
const second = PAUSESERVER('toggled');
$.json({ first: first, second: second });
}
function ping($) {
$.json({ ok: true });
}
```
## Business rules as text with NEWMACRO()
Source: https://forge.totaljs.es/en/total5/macros/
Framework: total5@0.0.19
Content-SHA256: d802bfef67ede8fdf42be2807b173b3732349293d75dcc93f6bd3a325d48c417
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`NEWMACRO(text, [nocompile], [isasync])` turns a few lines of a small language into a function `function(model, helpers)`. It is meant for rules that change more often than the code: a discount, a price, a permission check, a validation that someone who does not write JavaScript maintains in a file or in a database.
The language, as it was checked:
- `IF condition`, `ELSE IF condition`, `ELSE` and `FI` (the end of the condition) work as in a text-based rule. Conditions use `>`, `<`, `>=`, `<=`, `=` and `AND` / `OR`.
- `RETURN value` ends the macro with a value. `YES` and `NO` (also `TRUE`, `FALSE`, `OK`) are booleans, text between quotes is text and numbers are numbers.
- A word is a **property of the model**, written in lower case: `age` reads `model.age`, and `total = price * qty` writes `model.total`.
- `NAME(arguments)` calls a **helper**, the function with that name (lower case) in the `helpers` object you pass; inside it `this` is the model. With `isasync` set to `true` the macro is an async function and awaits the helpers.
- `#name` is a temporary variable of the run: `#temp = num + 1` and `RETURN #temp`.
- Use names of **several letters**: a property or a variable of one letter was not recognised in the checks.
`NEWMACRO()` compiles the text with `new Function`, so a macro is code: only compile text that you wrote or that a trusted person maintains, never text that a visitor sent. With `nocompile` set to `true` it returns the JavaScript that it generates instead of the function, which is useful to see what a rule does.
```js controllers/rules.js
const helpers = {
percent: function(value, rate) {
return value * rate / 100;
}
};
const age = NEWMACRO("IF age >= 18\n\tRETURN 'adult'\nELSE\n\tRETURN 'minor'\nFI");
const level = NEWMACRO("IF points > 100\n\tRETURN 'gold'\nELSE IF points > 50\n\tRETURN 'silver'\nELSE\n\tRETURN 'bronze'\nFI");
const access = NEWMACRO("IF active AND score >= 5\n\tRETURN yes\nFI\nRETURN no");
const total = NEWMACRO("total = price * qty\nIF qty >= 10\n\ttotal = total - PERCENT(total, 10)\nFI\nRETURN total");
const temporary = NEWMACRO("#next = count + 1\nRETURN #next");
const slow = NEWMACRO("RETURN LOOKUP(code)", false, true);
exports.install = function() {
ROUTE('POST /rules/', rules);
ROUTE('GET /source/', source);
};
async function rules($) {
const model = $.body;
$.json({
age: age(model, helpers),
level: level(model, helpers),
access: access(model, helpers),
total: total(model, helpers),
model_total: model.total,
temporary: temporary(model, helpers),
slow: await slow(model, { lookup: async code => 'found ' + code })
});
}
function source($) {
$.plain(NEWMACRO("IF points > 100\n\tRETURN 'gold'\nFI", true));
}
```
## Serve static files from the public folder
Source: https://forge.totaljs.es/en/total5/static-files/
Framework: total5@0.0.19
Content-SHA256: 01f26d711f253f04cfbb130f67bd1276c91264e83102714f80c7553ad9a9bb4d
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Everything inside the `public` directory is served as a static file, and the path inside the folder becomes the URL. No route is needed: `public/hello.txt` is available at `/hello.txt`.
```text public/hello.txt
Hello from the public folder
```
## Call another address with AJAX() from the server
Source: https://forge.totaljs.es/en/total5/ajax-requests/
Framework: total5@0.0.19
Content-SHA256: 761c3549df81c70a95b4da31e4770d745833c6240cfc45d2c310f79cd7d43d8f
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`AJAX('METHOD url', [data], [callback])` makes an HTTP request and gives you the **parsed** answer. It is the short form of `REQUEST()` for APIs that talk JSON: the same call that jComponent has in the browser, so the code of the page and of the server look alike.
- The address starts with an optional method: `AJAX('POST https://example.com/api/', data)`. Without a method it is a `GET`.
- `data`, when given, is sent as a **JSON body**. The request carries the header `x-requested-with: XMLHttpRequest`, so on the other side `$.xhr` is `true`.
- The answer is parsed by its content type: JSON becomes an object, `application/x-www-form-urlencoded` becomes an object, anything else arrives as text.
- When the status is 400 or more, the promise is **rejected** (or the callback gets the error first) with the parsed body of the answer as the error. A callback is called as `callback(err, value, response)`.
- Without a callback it returns a promise.
**For XML answers use `REQUEST()`** and parse the body yourself: `AJAX()` is made for JSON and text answers.
The example calls routes of the same application, whose address `$.hostname('/')` gives, so that it needs no other server.
```js controllers/calls.js
exports.install = function() {
ROUTE('GET /api/hello/', hello);
ROUTE('POST /api/echo/', echo);
ROUTE('GET /api/text/', text);
ROUTE('GET /api/xml/', xml);
ROUTE('GET /api/fail/', fail);
ROUTE('GET /calls/', calls);
};
function hello($) {
$.json({ hello: 'world', xhr: $.xhr });
}
function echo($) {
$.json({ received: $.body, xhr: $.xhr });
}
function text($) {
$.plain('plain text');
}
function xml($) {
$.xml('1');
}
function fail($) {
$.invalid('It is wrong');
}
async function calls($) {
const base = $.hostname('/');
const out = {};
out.hello = await AJAX('GET ' + base + 'api/hello/');
out.nomethod = await AJAX(base + 'api/hello/');
out.echo = await AJAX('POST ' + base + 'api/echo/', { a: 1 });
out.text = await AJAX(base + 'api/text/');
try {
await AJAX(base + 'api/fail/');
} catch (err) {
out.error = err.message || err;
}
out.callback = await new Promise(resolve => AJAX(base + 'api/hello/', (err, value, response) => resolve({ err: err, hello: value.hello, status: response.status })));
out.xml = await Promise.race([
AJAX(base + 'api/xml/').then(() => 'answered', () => 'rejected'),
new Promise(resolve => setTimeout(() => resolve('never answered'), 1000))
]);
$.json(out);
}
```
## Call other HTTP APIs with RESTBuilder
Source: https://forge.totaljs.es/en/total5/restbuilder/
Framework: total5@0.0.19
Content-SHA256: 12c13b9eec2ed90f374077ed6b677b5de7172b1bd97ef4b605591216bfc1e24c
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`RESTBuilder.GET(url)`, `.POST(url, data)`, `.PUT`, `.PATCH` and `.DELETE` build a request. Data of a POST is sent as JSON. Chain `.header(name, value)`, `.cookie(name, value)`, `.timeout(milliseconds)` and `.auth(user, password)` before `.exec(function(err, response, output) {})` (or `.promise()` for `await`).
- `response` is the body, parsed automatically when it is JSON
- `output.status` is the HTTP status and `output.headers` the response headers
- `err` is set for network errors and for HTTP error statuses
The example calls two routes of its own app so it can run anywhere; use the address of the real service in your code. `$.hostname(path)` builds the full URL of this server.
```js controllers/service.js
exports.install = function() {
ROUTE('GET /api/hello/', hello);
ROUTE('POST /api/sum/', sum);
ROUTE('GET /call/', call);
};
function hello($) {
$.json({ message: 'hello', agent: $.headers['x-agent'] || null });
}
function sum($) {
$.json({ sum: $.body.a + $.body.b });
}
async function call($) {
var greeting = await RESTBuilder.GET($.hostname('/api/hello/')).header('x-agent', 'forge').timeout(5000).promise();
var total = await RESTBuilder.POST($.hostname('/api/sum/'), { a: 20, b: 22 }).promise();
RESTBuilder.GET($.hostname('/api/missing/')).exec(function(err, response, output) {
$.json({ greeting: greeting, total: total, missing: output.status });
});
}
```
## Release a stream when it ends with CLEANUP()
Source: https://forge.totaljs.es/en/total5/cleanup-streams/
Framework: total5@0.0.19
Content-SHA256: dcaf443c6e5b758eb7c3a4ea92ba9c0289551a3b46dcb0c1e3d642c51e142f37
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`CLEANUP(stream, [callback])` waits until a stream has **finished** (it ended, it was closed or it failed) and then destroys it, which frees the file handle or the socket behind it. With a callback it is called after that; without one it returns a promise that resolves at that moment. It is the call to make after you hand a stream to something that reads it (a response, a pipe, an upload) when you want to be sure that nothing stays open if the other side stops early.
```js controllers/streams.js
const Fs = require('fs');
const { PassThrough } = require('stream');
exports.install = function() {
ROUTE('GET /copy/', copy);
ROUTE('GET /copy-callback/', copycallback);
};
async function copy($) {
const file = PATH.tmp('cleanup.txt');
Fs.writeFileSync(file, 'some text to read');
const stream = Fs.createReadStream(file);
const chunks = [];
// a sink that reads everything
const sink = new PassThrough();
sink.on('data', chunk => chunks.push(chunk));
stream.pipe(sink);
const before = stream.destroyed;
await CLEANUP(stream);
$.json({ before: before, after: stream.destroyed, content: Buffer.concat(chunks).toString() });
}
function copycallback($) {
const file = PATH.tmp('cleanup.txt');
Fs.writeFileSync(file, 'again');
const stream = Fs.createReadStream(file);
stream.resume();
CLEANUP(stream, () => $.json({ destroyed: stream.destroyed }));
}
```
## Translate a view into several languages
Source: https://forge.totaljs.es/en/total5/localization/
Framework: total5@0.0.19
Content-SHA256: 46d3cf632dac9d720b148f223066319e74bd1173b10dbf1d0d75d85facde901e
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Wrap any text of a view in `@(...)` and Total.js replaces it with the translation for the current language. Translations live in `resources/.resource`; each line is `key : translation`. The key is `T` plus the hash of the original text in base 36, which `total5` can generate for you. Texts without a translation stay as written, so the view text acts as the default language.
`LOCALIZE()` decides the language of each request. Here it is read from the query string, but it can be a cookie, a header or a URL segment.
```js definitions/localization.js
LOCALIZE(function($) {
return $.query.lang;
});
```
```text resources/sk.resource
// Total.js translation file
T4oxobw : Vitajte v obchode
```
```html views/index.html
@{layout('')}
@(Welcome to the shop)
```
```js controllers/default.js
exports.install = function() {
ROUTE('GET /', home);
};
function home($) {
$.view('index');
}
```
## Run code on a schedule with CRON
Source: https://forge.totaljs.es/en/total5/cron-jobs/
Framework: total5@0.0.19
Content-SHA256: 1e42e869db5a82463754c80c867472d8194d27b6f0858093ca51ce16e18ded54
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`CRON(line, fn)` runs `fn(date)` whenever the current time matches `line`. The line has five fields, as in Unix cron: `minute hour day-of-month month day-of-week`. Each field accepts `*` (any value), `1,5,9` (a list), `1-5` (a range) or an exact number. Day names such as `mon` or `fri` work in the last field.
**Write the values as a list**, `0,15,30,45`: it matches exactly the times you name.
Total.js checks the registered jobs once a minute, so the finest resolution is one minute. `CRON()` returns an object: `job.remove()` unregisters it and `job.check(date)` tells you whether a date matches the line, which is also the easiest way to test a schedule without waiting for it.
```js controllers/cron.js
exports.install = function() {
ROUTE('GET /cron/matches/', matches);
ROUTE('GET /cron/runs/', runs);
};
var counter = { runs: 0 };
// Every 15 minutes, from Monday to Friday, between 09:00 and 17:59
var job = CRON('0,15,30,45 9-17 * * 1-5', function(date) {
counter.runs++;
});
function matches($) {
var dates = {
monday_0930: new Date(2026, 0, 5, 9, 30),
monday_0931: new Date(2026, 0, 5, 9, 31),
monday_1830: new Date(2026, 0, 5, 18, 30),
sunday_0930: new Date(2026, 0, 4, 9, 30)
};
var output = {};
for (var key in dates)
output[key] = job.check(dates[key]);
$.json(output);
}
function runs($) {
$.json(counter);
}
```
## Run heavy work in a thread or a child process
Source: https://forge.totaljs.es/en/total5/workers-and-threads/
Framework: total5@0.0.19
Content-SHA256: 6be8eed8f5c6fc8d1a9a88a066519dbcd1b8884933677a534fc44d7c4d95f0d1
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Node.js runs your code on one thread, so a long calculation blocks every request. `NEWTHREAD(name, data)` starts `workers/.js` in a worker thread and returns a standard Node.js `Worker`: send it messages with `worker.postMessage(value)`, receive replies with `worker.on('message', fn)`, and stop it with `worker.kill()`. The second argument (`data`) is handed to the worker as its starting data. `NEWFORK(name)` has the same shape but starts a separate child process, which is isolated from the main process (a crash in it does not affect your app).
Inside the worker file, `NEWTHREAD()` without arguments returns the worker handle: `worker.data` holds the starting data, `worker.message = fn` receives messages and `worker.postMessage(value)` answers. Pass a name prefixed with `~` to use a file by path instead of `workers/.js`.
```js workers/sum.js
require('total5');
const worker = NEWTHREAD();
const start = worker.data.start;
worker.message = function(message) {
let total = start;
for (let i = 1; i <= message.to; i++)
total += i;
worker.postMessage({ total: total });
};
```
```js controllers/sum.js
exports.install = function() {
ROUTE('GET /sum/{to}/', sum);
};
var thread = NEWTHREAD('sum', { start: 1000 });
function sum($) {
var to = +$.params.to;
var timeout = setTimeout(() => $.invalid('timeout'), 4000);
thread.once('message', function(response) {
clearTimeout(timeout);
$.json(response);
});
thread.postMessage({ to: to });
}
```
## Share a few worker threads with NEWTHREADPOOL()
Source: https://forge.totaljs.es/en/total5/thread-pool/
Framework: total5@0.0.19
Content-SHA256: 6fbb579ec2a9b0a43ee3b6a86ea83738bde754d17a73392805d15250c0db98d6
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`NEWTHREAD()` starts one worker. When many requests need the same heavy job, one worker per request is too many and a single worker makes them wait for each other without any order. `NEWTHREADPOOL(name, count, [isfork])` starts `count` workers of `workers/.js` (a thread; with `isfork` set to `true` a child process, like `NEWFORK()`) and hands them out one at a time.
- `pool.exec(function(worker, release) {})` waits until a worker is free and calls your function with it. Send it the work with `worker.postMessage(value)`, read the answer with `worker.once('message', fn)` and **call `release()` when you are done**: the worker goes back to the pool and the next waiting job takes it.
- Without a function, `pool.exec()` returns a promise that resolves with `worker` (and the worker has `worker.release()`), so it works with `await`.
- The jobs are served in the order in which they asked. The pool does not queue the messages of a worker: one job at a time has it.
- The pool is also kept in `F.workers[name]`. `pool.workers` is the list of the workers.
The worker file is the same as for `NEWTHREAD()`: `NEWTHREAD()` inside it returns the handle with `worker.message` and `worker.postMessage()`. Here every worker adds up the numbers and says which thread did it.
```js workers/total.js
require('total5');
const worker = NEWTHREAD();
worker.message = function(message) {
let total = 0;
for (let i = 1; i <= message.to; i++)
total += i;
worker.postMessage({ to: message.to, total: total, thread: require('worker_threads').threadId });
};
```
```js controllers/total.js
exports.install = function() {
ROUTE('GET /total/{to}/', total);
ROUTE('GET /await/{to}/', total_await);
ROUTE('GET /workers/', workers);
};
const pool = NEWTHREADPOOL('total', 2);
function total($) {
pool.exec(function(worker, release) {
worker.once('message', function(response) {
release();
$.json(response);
});
worker.postMessage({ to: +$.params.to });
});
}
async function total_await($) {
const worker = await pool.exec();
worker.once('message', function(response) {
worker.release();
$.json(response);
});
worker.postMessage({ to: +$.params.to });
}
function workers($) {
$.json({ workers: pool.workers.length, registered: F.workers.total === pool });
}
```
## Call other servers with REQUEST and save files with DOWNLOAD
Source: https://forge.totaljs.es/en/total5/request-and-download/
Framework: total5@0.0.19
Content-SHA256: 53c1ccdf8b9d50fa08069f0f719cced7ec9528bd28bd251c9307fff966f9aaec
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`REQUEST(options)` is the low-level HTTP client that `RESTBuilder` is built on. Pass an object and `await` it (or give it a callback as the second argument). The useful options are `url`, `method` (default `GET`), `query` (an object, appended to the URL), `headers`, `cookies`, `body` (a string or a Buffer), `type` (`json`, `text`, `html`, `xml`, `urlencoded` or `raw`, which sets `content-type`), `timeout` in milliseconds and `noredirect`.
The result is an object with `status`, `headers`, `cookies` and `body` (a string; parse JSON yourself with `.parseJSON()`). A response with an error status such as 404 is **not** an error: check `status`. The promise only rejects when the request itself fails (connection refused, timeout, invalid host).
`DOWNLOAD(url, filename)` streams the response into a file and resolves with `{ status, size, filename }`. It does not look at the status, so check it before using the file.
```js controllers/request.js
exports.install = function() {
ROUTE('GET /remote/', remote);
ROUTE('POST /remote/ @json', remote_post);
ROUTE('GET /remote/text/', remote_text);
ROUTE('GET /call/', call);
ROUTE('GET /download/', download);
};
function remote($) {
$.json({ hello: 'world', name: $.query.name || null });
}
function remote_post($) {
$.json({ received: $.body, token: $.headers['x-token'] || null });
}
function remote_text($) {
$.plain('first line\nsecond line\n');
}
async function call($) {
var get = await REQUEST({ url: $.hostname('/remote/'), query: { name: 'Louis' } });
var post = await REQUEST({ method: 'POST', url: $.hostname('/remote/'), type: 'json', body: JSON.stringify({ a: 1 }), headers: { 'x-token': 'abc' } });
var missing = await REQUEST({ url: $.hostname('/does-not-exist/') });
var failed = null;
try {
await REQUEST({ url: 'http://127.0.0.1:1/' });
} catch (e) {
failed = true;
}
$.json({
get: get.body.parseJSON(),
post: post.body.parseJSON(),
status: [get.status, missing.status],
failed: failed
});
}
async function download($) {
var filename = PATH.tmp('forge-download.txt');
var response = await DOWNLOAD($.hostname('/remote/text/'), filename);
var content = require('fs').readFileSync(filename).toString('utf8');
$.json({ status: response.status, size: response.size, content: content });
}
```
## Send an email from a view
Source: https://forge.totaljs.es/en/total5/send-email/
Framework: total5@0.0.19
Content-SHA256: 82d673e47df8e2c89407aa68da87b54706364712401f9ee8742dd3918b58e706
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js sends mail over SMTP. The server and the sender go in the `config` file: `mail_smtp` is the server, `mail_smtp_options` an object with the rest of the connection settings (`port`, `user`, `password`, `secure`...) and `mail_from` / `mail_from_name` the sender. `mail_reply`, `mail_cc` and `mail_bcc` add the same headers to every message.
`MAIL(email, subject, view, model, [language], [callback])` renders `views/.html` with the model (the same views as for pages, including `@(...)` localization) and sends the result as the HTML body. `email` can be one address or several separated by commas. `HTMLMAIL(email, subject, html, [language], [callback])` sends a string you built yourself. Both return the message; `callback(err)` runs when the server has accepted or rejected it, and `err` is `null` on success.
Messages are queued and sent right after the current tick, so `MAIL()` never blocks the request. Wait for the callback only when the user has to know whether it worked.
```text config
name : Mail example
mail_smtp : 127.0.0.1
mail_smtp_options : { port: 2526 }
mail_from : hello@example.com
mail_from_name : Example
```
The view is rendered **with the layout**, like any page, so `views/layout.html` must exist (the app crashes the request with an `ENOENT` error otherwise). A mail layout can be as small as the one below, or you can use your page layout.
```html views/layout.html
@{body}
```
```html views/welcome.html
Welcome, @{model.name}!
Your account is ready.
```
```js controllers/mail.js
exports.install = function() {
ROUTE('POST /welcome/ @json', welcome);
ROUTE('POST /note/ @json', note);
};
function welcome($) {
MAIL($.body.email, 'Welcome aboard', 'welcome', { name: $.body.name }, function(err) {
$.json({ sent: !err, error: err ? err.toString() : null });
});
}
function note($) {
HTMLMAIL($.body.email, 'A short note', 'Plain HTML message
', function(err) {
$.json({ sent: !err, error: err ? err.toString() : null });
});
}
```
## Customize the 404 and 401 pages
Source: https://forge.totaljs.es/en/total5/error-pages/
Framework: total5@0.0.19
Content-SHA256: 379ed76a90c99ff5446f0f9679ef0c1fd1662cde467d0b7e43332f76bcc6db32
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
When Total.js cannot serve a request it shows a built-in error page. A route whose URL is `#` plus the status code replaces it: `ROUTE('#404', fn)` for "not found", `ROUTE('#401', fn)` for "not authorized", and the same for `#400`, `#431` and others. The handler receives `$` like any route, but **you set the status yourself** with `$.response.status`: Total.js does not do it for you when a custom handler exists. Without a custom handler the built-in page is used and AJAX requests (header `x-requested-with`) get a JSON error instead of HTML.
`$.fallback(code)` sends the current request to the handler for `code`, which is how you answer 404 for a record that does not exist or 403 for a forbidden resource from inside your own code.
```js controllers/errors.js
exports.install = function() {
ROUTE('#404', not_found);
ROUTE('#401', unauthorized);
ROUTE('GET /items/{id}/', item);
ROUTE('+GET /private/', private_page);
};
var items = { '1': 'First item' };
function item($) {
var name = items[$.params.id];
if (name)
$.json({ name: name });
else
$.fallback(404);
}
function private_page($) {
$.plain('secret');
}
function not_found($) {
$.response.status = 404;
$.html('Nothing here
' + $.url.encode() + ' does not exist.
');
}
function unauthorized($) {
$.response.status = 401;
$.json({ error: 'Please sign in first' });
}
AUTH(function($) {
$.invalid();
});
```
## Mail yourself a message with LOGMAIL()
Source: https://forge.totaljs.es/en/total5/logmail/
Framework: total5@0.0.19
Content-SHA256: 28902e3036b56ddecd785939f8350520d582fbf2dea6e5755e97920246b2352e
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`LOGMAIL(email, [subject], body, [callback])` sends a message made for the developer, not for a customer: no view, no layout. The body is a text. The text goes inside a ``, in a small HTML page, so line breaks and spaces survive; it is put as it is, so do not send untrusted HTML. Without a `subject` the subject is the name of the application (`name` in the `config`).
**Pass the body as a text.** For an object use `JSON.stringify(object, null, 2)`, as the second route below does.
It uses the same SMTP server, sender and queue as `MAIL()` (`mail_smtp`, `mail_smtp_options`, `mail_from` in the `config`). It is meant for things like "the import failed" with the data that you need to see.
```text config
name : Log mail example
mail_smtp : 127.0.0.1
mail_smtp_options : { port: 2527 }
mail_from : hello@example.com
mail_from_name : Example
```
```js controllers/log.js
exports.install = function() {
ROUTE('POST /alert/ @json', alert);
ROUTE('POST /alert-object/ @json', alert_object);
ROUTE('POST /alert-raw/ @json', alert_raw);
};
function alert($) {
LOGMAIL('ops@example.com', 'Import failed', 'Line 1\nLine 2', function(err) {
$.json({ sent: !err, error: err ? err.toString() : null });
});
}
function alert_object($) {
// without a subject: the subject is the name of the application; the object is written by us
LOGMAIL('ops@example.com', JSON.stringify({ rows: 12, failed: ['a', 'b'] }, null, 2), function(err) {
$.json({ sent: !err, error: err ? err.toString() : null });
});
}
function alert_raw($) {
// the object itself: it throws in total5 0.0.19
try {
LOGMAIL('ops@example.com', { rows: 12 }, NOOP);
$.json({ thrown: false });
} catch (e) {
$.json({ thrown: true, error: e.message });
}
}
```
## Send files and streams to the client
Source: https://forge.totaljs.es/en/total5/file-download/
Framework: total5@0.0.19
Content-SHA256: c21ebfd97c7af12b738ceba0374203c527f79fc70b1373c215c4a4462f8009d4
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`$.file(path, [download])` sends a file from disk. The `content-type` comes from the extension. Pass `true` as the second argument to make the browser download the file under its own name, or a string to choose the name (`attachment; filename*=...` is set for you, UTF-8 safe). A path that does not exist ends with the 404 page.
`$.stream(type, stream, [download])` sends any readable stream with the given content type: a generated file, a response from another server or a database export. It sets no length, so the response is chunked. `PATH.public()`, `PATH.private()` and `PATH.tmp()` build absolute paths into the project folders; files in `public/` are already served without any route, so use `$.file()` for the files that must stay private or need a check first.
```js controllers/download.js
const Fs = require('fs');
exports.install = function() {
ROUTE('GET /report/', report);
ROUTE('GET /report/save/', report_save);
ROUTE('GET /export/', export_csv);
ROUTE('GET /missing/', missing);
};
function filepath() {
var filename = PATH.tmp('report.txt');
Fs.writeFileSync(filename, 'Total.js report\n');
return filename;
}
function report($) {
$.file(filepath());
}
function report_save($) {
$.file(filepath(), 'my report.txt');
}
function export_csv($) {
var stream = Fs.createReadStream(filepath());
$.stream('text/csv', stream, 'export.csv');
}
function missing($) {
$.file(PATH.tmp('does-not-exist.txt'));
}
```
## Forward requests to another server
Source: https://forge.totaljs.es/en/total5/proxy/
Framework: total5@0.0.19
Content-SHA256: a5143999f45cb88cba286cb0dfca70e97c0f40c381830c2860aeb38043de9342
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`PROXY(prefix, target)` forwards every request below a URL prefix to another server and streams the answer back, so one Total.js app can sit in front of several services. It runs before the routes, so a prefix handled by `PROXY()` never reaches `ROUTE()`. The request is passed on with its method, headers and body; the headers `x-forwarded-for`, `x-forwarded-url` and `x-forwarded-host` tell the upstream about the original request. `PROXY(prefix)` without a target removes the proxy.
The object it returns configures the proxy: `.copy(type)` chooses which part of the path is sent upstream (see below), `.timeout(ms)` limits the wait, `.check(fn)` makes the proxy apply only when `fn(ctrl)` returns true and `.before(uri, ctrl)` can change the outgoing request, for example to add a header.
`.copy()` takes `none` (the default), `extend` or `replace`. With **`none`** the upstream only receives the path of the target plus the query string: `/none/a/b/?q=1` becomes `/base/?q=1` for the target `http://host/base/`. With **`extend`** the rest of the request path is appended to the target path: `/extend/a/b/?q=1` becomes `/base/a/b/?q=1`, which is what you want for a gateway. `replace` behaves like `extend` when the target has a path of its own; when the target is just a host it sends the rest of the path **without the query string**, so prefer `extend`.
For a single route use `$.proxy(url)`: it sends the current request (method, query string, body and headers) to `url` and returns the upstream answer with its status code.
```js controllers/proxy.js
const Http = require('http');
// A small upstream server that reports what it received
Http.createServer(function(req, res) {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ method: req.method, url: req.url, token: req.headers['x-token'] || null, forwarded: !!req.headers['x-forwarded-url'] }));
}).listen(9411, '127.0.0.1');
// One proxy per copy mode, to see the difference (the target has the path /base/)
PROXY('/none/', 'http://127.0.0.1:9411/base/');
PROXY('/extend/', 'http://127.0.0.1:9411/base/').copy('extend');
PROXY('/plain/', 'http://127.0.0.1:9411/base/').copy('replace');
// Everything below /service/ goes to the upstream, and a header is added on the way
PROXY('/service/', 'http://127.0.0.1:9411/').copy('extend').before(function(uri, ctrl) {
uri.headers['x-token'] = 'secret';
});
exports.install = function() {
ROUTE('GET /single/', single);
};
function single($) {
$.proxy('http://127.0.0.1:9411/from-route/');
}
```
## Publish a local server through a tunnel with PROXYSERVER() and PROXYCLIENT()
Source: https://forge.totaljs.es/en/total5/proxy-tunnel/
Framework: total5@0.0.19
Content-SHA256: 6c7bccccd48386b830d20ab4b33001d93abc21834308e84c8fd99c0210bab471
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`PROXY()` forwards a request to a server that the proxy can reach. A tunnel is for the opposite case: the program is on a laptop or behind a firewall, so nobody can connect to it, but it can connect out. `PROXYSERVER()` and `PROXYCLIENT()` make that with one WebSocket that the private side opens:
- `PROXYSERVER([endpoint], [socketendpoint], [logger])` runs on the public server. Every request below `endpoint` (default `/`) is sent through the tunnel; `socketendpoint` (default `/`) is the WebSocket address where the client connects. Use **different** addresses when the same application serves anything else: a proxy on `/` takes every request, also the ones of your own routes (it runs before them). `logger` is a function (default: prints with the date); pass `null` for none.
- `PROXYCLIENT(serverurl, port, [logger])` runs on the private side (in this example the address of the server is built with `CONF.$port`, the port of the application). `serverurl` is the address of the WebSocket of the server (`ws://` or `wss://`; `http` is changed to `ws`) and `port` is `3000` or `ip:3000`, the local program that receives the requests. It returns the WebSocket client.
- **One client at a time:** when a second client connects, the server closes it with the code `4001`. Without a client the answer of the server is `Not yet available.`.
- The local program receives the **whole path of the original request**, including the `endpoint` (`/tunnel/hello`), the headers of the visitor and an `x-proxy: Total.js` header, with `host` set to the local address.
- The WebSocket route has no authentication of its own: do not publish it without something in front of it that decides who can connect (the address is the secret, a `check` of your own, a firewall).
Here one application is both sides: a small local program on port 9511 (any program would do), the server on `/tunnel/` and the client that connects to the same application.
```js definitions/local.js
// The program that is "private": an ordinary HTTP server that only listens on the loopback address
require('http').createServer(function(req, res) {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ path: req.url, via: req.headers['x-proxy'] || null, host: req.headers.host }));
}).listen(9511, '127.0.0.1');
```
```js controllers/tunnel.js
exports.install = function() {
ROUTE('GET /status/', status);
ROUTE('POST /connect/', connect);
};
// The public side: /tunnel/* goes through the tunnel, the WebSocket of the client is /tunnel-socket/. No logger.
PROXYSERVER('/tunnel/', '/tunnel-socket/', null);
let client = null;
function connect($) {
client = PROXYCLIENT('ws://127.0.0.1:' + CONF.$port + '/tunnel-socket/', '9511', null);
$.json({ connecting: true });
}
function status($) {
$.json({ connected: !!client });
}
```
## Protect forms and requests with CSRF tokens
Source: https://forge.totaljs.es/en/total5/csrf-protection/
Framework: total5@0.0.19
Content-SHA256: e10434ff98f2737ef66f4645652b5c226dd2bc4b75b4c4a3bc5d348b2d366762
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A CSRF attack makes a logged-in user's browser send a request they never intended. The defence is a token that the page receives from your server and that has to come back with every state-changing request. Total.js creates and verifies the token for you, but only when `secret_csrf` is set in the `config` file (without it the check always passes).
`$.csrf()` returns a token (and `@{csrf}` prints it in a view). It is an encrypted value tied to the **IP address** and the **user agent** of the client and it expires after the time in `$csrfexpiration` (30 minutes by default). `DEF.onCSRFcheck($)` verifies the token that came back, read from the `x-csrf-token` header or the `csrf` query string parameter, and returns `true` or `false`. For ordinary routes you call it yourself, most conveniently from a middleware that you add with `#csrf` to every route that changes data. WebSocket routes with the `@csrf` flag are checked automatically.
```text config
secret_csrf : change-this-secret-in-production
```
```js definitions/csrf.js
MIDDLEWARE('csrf', function($, next) {
if (DEF.onCSRFcheck($))
next();
else
$.invalid(403);
});
```
```js controllers/csrf.js
exports.install = function() {
ROUTE('GET /form/', form);
ROUTE('POST /transfer/ @json #csrf', transfer);
};
function form($) {
$.json({ csrf: $.csrf() });
}
function transfer($) {
$.json({ ok: true, amount: $.body.amount });
}
```
## Group actions in a schema and reuse input definitions
Source: https://forge.totaljs.es/en/total5/schemas/
Framework: total5@0.0.19
Content-SHA256: 951ce36fc3ea0cd16be8f91ca9a8f5e5b115d12205262a6c67c6e3f7ed178117
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`NEWSCHEMA(name, fn)` groups the actions of one entity. The function receives the schema object and `schema.action(name, meta)` registers an action called `Name/action`, with the same options as `NEWACTION()` (`input`, `output`, `params`, `user`, `permissions`, `action`...). Everywhere else the action is used by its full name, exactly as if it had been declared with `NEWACTION('Users/insert', ...)`: `ROUTE('POST /users/ --> *Users/insert')` or `ACTION('Users/insert', payload)`.
A fragment of an input definition that several actions share, such as an address, goes in its own file in `jsonschemas/`. A `.txt` file holds the same `name:Type` list as `input`; the name of the file is the name of the schema. Reference it from an `input` with `@Name`, and its fields (and which of them are required) are added to the action's own fields.
```text jsonschemas/Address.txt
*street:String, *city:String, zip:String
```
```js schemas/users.js
NEWSCHEMA('Users', function(schema) {
schema.action('insert', {
name: 'Create a user',
input: '*name:String, @Address',
output: 'id:UID, name:String, city:String',
action: function($, model) {
$.callback({ id: UID(), name: model.name, city: model.city });
}
});
schema.action('count', {
name: 'Count users',
action: function($) {
$.callback({ count: 2 });
}
});
});
```
```js controllers/users.js
exports.install = function() {
ROUTE('POST /users/ --> *Users/insert');
ROUTE('GET /users/count/ --> *Users/count');
};
```
## Build a processing pipeline with transforms
Source: https://forge.totaljs.es/en/total5/transforms/
Framework: total5@0.0.19
Content-SHA256: c9dfc7d277360f14463537cdbe813208627c535f75555adae2c6f31bf79557f3
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A transform is a named chain of steps. `NEWTRANSFORM(name, fn, [id])` adds a step to the chain `name`; steps run in the order they were registered, and each one receives the options object `$` and the current value. Change the value with `$.value = ...` and call `$.next()` to continue with the next step. `$.invalid(message)` adds an error. `TRANSFORM(name, value)` starts the chain and returns a promise with the final value; the promise rejects when a step has reported an error. With a callback instead, `TRANSFORM(name, value, function(err, value) {})`. A name without any step returns the value as it is.
Use it for work that has to be extended from several places, such as cleaning a comment before it is saved: a spam filter, a trimmer and a link checker can each live in their own file and be added or removed without touching the code that calls `TRANSFORM()`. The optional `id` lets you remove one step later with `NEWTRANSFORM(name, null, id)`.
```js definitions/transforms.js
NEWTRANSFORM('comment', function($, value) {
$.value = value.trim();
$.next();
}, 'trim');
NEWTRANSFORM('comment', function($, value) {
if (/spam/i.test(value))
$.invalid('Spam is not allowed');
$.next();
}, 'spam');
NEWTRANSFORM('comment', function($, value) {
$.value = value.replace(/\s+/g, ' ');
$.next();
}, 'spaces');
```
```js controllers/comments.js
exports.install = function() {
ROUTE('POST /comment/ @json', comment);
ROUTE('DELETE /comment/spaces/', remove_step);
};
async function comment($) {
try {
var text = await TRANSFORM('comment', $.body.text);
$.json({ text: text });
} catch (err) {
$.json({ error: true });
}
}
function remove_step($) {
NEWTRANSFORM('comment', null, 'spaces');
$.success();
}
```
## Exchange messages with the Total Messaging Service
Source: https://forge.totaljs.es/en/total5/tms/
Framework: total5@0.0.19
Content-SHA256: 821100b4707c6d136070bfa640d0a05c702215e66bf7abe7101bcf0f64bf3ca0
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
TMS (Total Messaging Service) is a small message bus on top of a WebSocket. Turn it on in the `config` file with `$tms : true`; the endpoint is `/$tms/` and `secret_tms` is the token that every client must send in the `x-token` header (a wrong token closes the connection with code 4001, and an address that fails more than 5 times is blocked for a while). When a client connects it receives a `meta` message that lists what the app publishes, what it accepts and what it can do.
Three things can be exposed, each one described by a schema in the same `name:Type` format as an action `input`:
- `NEWPUBLISH(name, schema)` declares a message the app **sends**; `PUBLISH(name, value)` sends it to every client that asked for it by sending `{ type: 'subscribers', subscribers: [name] }`.
- `NEWSUBSCRIBE(name, schema, callback)` declares a message the app **receives**: a client sends `{ type: 'subscribe', id: name, data: {...} }` and `callback(message, client)` runs only when the data is valid. The validated data is in `message.response` (`message.error` is always `null` there) and `client` is the sender.
- `NEWCALL(name, schema, callback)` declares a function the client **calls**: the client sends `{ type: 'call', id: name, data: {...}, callbackid: '1' }`, and the app answers with the same message carrying `success: true` and the result in `data`, or `error: true` when the data is invalid. `callback(data, done, client)` produces the result with `done(null, result)`.
Passing `null` as the schema of `NEWPUBLISH` or `NEWSUBSCRIBE` removes the declaration. The official client of Total.js (`TMSCLIENT`) speaks this protocol for you from another Total.js app; any WebSocket client works.
`UNSUBSCRIBE(name, [callback])` removes the listeners of a message that the app receives: only `callback` when you pass it, all of them when you do not. It returns `true` when it removed something and `false` when there was nothing to remove.
```text config
$tms : true
secret_tms : my-tms-token
```
```js definitions/tms.js
NEWPUBLISH('news', 'title:String');
NEWSUBSCRIBE('orders', 'id:String, qty:Number', function(message, client) {
MAIN.orders.push(message.response);
});
NEWCALL('sum', 'a:Number, b:Number', function(data, done) {
done(null, { total: data.a + data.b });
});
MAIN.orders = [];
```
```js controllers/tms.js
exports.install = function() {
ROUTE('POST /news/ @json', news);
ROUTE('GET /orders/', orders);
ROUTE('GET /unsubscribe/', unsubscribe);
};
function news($) {
PUBLISH('news', { title: $.body.title });
$.success();
}
function orders($) {
$.json(MAIN.orders);
}
function unsubscribe($) {
const first = UNSUBSCRIBE('orders');
const second = UNSUBSCRIBE('orders');
$.json({ first: first, second: second });
}
```
## Build a complete REST resource with actions
Source: https://forge.totaljs.es/en/total5/actions-crud/
Framework: total5@0.0.19
Content-SHA256: 54d1adaf485ed805011ae1c7cc61741866509242ce858ba3819af61ed0065ab5
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A REST resource is six operations: list (`query`), `read`, `insert`, `update` (replace), `patch` (change a few fields) and `remove`. In Total.js each one is an action with its own validation, so the route table stays a plain map from URL to action. `params` validates the id of the URL, `query` the filters of the list, `input` the body. Inside an action `$.params`, `$.query` and the validated body (second argument of `action`) are already clean values.
Report a missing record with `$.invalid(404)`: it answers with the status code and a JSON error, the same way for every action. `$.success()` answers `{ "success": true }` and `$.callback(value)` answers with a value, which is also filtered by the `output` of the action. `UID()` creates the id of a new record.
**`patch` needs `partial: true`.** Without it a field that was not sent is still filled with the default of its type (`''` for a `String`), and `update()` would overwrite the stored value with that empty text. With `partial: true` only the fields present in the request reach the action.
```js actions/products.js
NEWACTION('Products/query', {
name: 'List products',
query: 'page:Number, search:String',
output: 'items:[id:UID, name:String, price:Number], page:Number',
action: function($) {
var page = $.query.page || 1;
var builder = NOSQL('products').find().sort('name').take(10).skip((page - 1) * 10);
if ($.query.search)
builder.search('name', $.query.search);
builder.callback((err, items) => $.callback({ items: items, page: page }));
}
});
NEWACTION('Products/read', {
name: 'Read a product',
params: '*id:UID',
output: 'id:UID, name:String, price:Number',
action: function($) {
NOSQL('products').read().where('id', $.params.id).callback(function(err, item) {
if (item)
$.callback(item);
else
$.invalid(404);
});
}
});
NEWACTION('Products/insert', {
name: 'Create a product',
input: '*name:String, *price:Number',
action: function($, model) {
model.id = UID();
NOSQL('products').insert(model).callback(() => $.callback({ id: model.id }));
}
});
NEWACTION('Products/update', {
name: 'Replace a product',
params: '*id:UID',
input: '*name:String, *price:Number',
action: function($, model) {
NOSQL('products').update(model).where('id', $.params.id).callback(function(err, count) {
if (count)
$.success();
else
$.invalid(404);
});
}
});
NEWACTION('Products/patch', {
name: 'Change some fields of a product',
partial: true,
params: '*id:UID',
input: 'name:String, price:Number',
action: function($, model) {
NOSQL('products').update(model).where('id', $.params.id).callback(function(err, count) {
if (count)
$.success();
else
$.invalid(404);
});
}
});
NEWACTION('Products/remove', {
name: 'Remove a product',
params: '*id:UID',
action: function($) {
NOSQL('products').remove().where('id', $.params.id).callback(function(err, count) {
if (count)
$.success();
else
$.invalid(404);
});
}
});
```
```js controllers/products.js
exports.install = function() {
ROUTE('GET /products/ --> *Products/query');
ROUTE('GET /products/{id}/ --> *Products/read');
ROUTE('POST /products/ --> *Products/insert');
ROUTE('PUT /products/{id}/ --> *Products/update');
ROUTE('PATCH /products/{id}/ --> *Products/patch');
ROUTE('DELETE /products/{id}/ --> *Products/remove');
};
```
## Configure a WebSocket route with flags, auth and parameters
Source: https://forge.totaljs.es/en/total5/websocket-flags/
Framework: total5@0.0.19
Content-SHA256: 153249a5572bf8c2fd9e77659ca65a7b282a94a9600af87f3e59004c290776ab
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A `SOCKET` route takes the same building blocks as an HTTP route. By default messages are JSON objects. `@text` makes the incoming messages plain strings and `@binary` makes them `Buffer`s. In a text route `send(message)` still encodes what it sends as JSON, so a string arrives with quotes; pass `true` as the second argument, `send(message, true)`, to send the string as it is. A `+` before `SOCKET` accepts only users that `AUTH()` approves (the connection is refused otherwise), and `{name}` segments are available in `self.params`. `@csrf` checks a CSRF token before the connection is accepted: send it as the `csrf` query string parameter (`/secure/?csrf=...`), and see the CSRF recipe for `secret_csrf` and `$.csrf()`. A middleware is attached with `#name` as on any route.
```text config
secret_csrf : change-this-secret-in-production
```
```js definitions/auth.js
AUTH(function($) {
if ($.query.token === 'letmein')
$.success({ id: '1', name: 'Louis' });
else
$.invalid();
});
```
```js controllers/sockets.js
exports.install = function() {
ROUTE('SOCKET /text/ @text', text);
ROUTE('SOCKET /bin/ @binary', binary);
ROUTE('+SOCKET /private/', private_socket);
ROUTE('SOCKET /room/{name}/', room);
ROUTE('SOCKET /secure/ @csrf', secure);
ROUTE('GET /where/', where);
ROUTE('GET /csrf/', csrf);
};
function text() {
this.on('message', function(client, message) {
client.send(typeof(message) + ':' + message.toUpperCase(), true);
});
}
function binary() {
this.on('message', function(client, message) {
client.send(Buffer.from(Buffer.isBuffer(message) ? 'buffer:' + message.length : 'other'));
});
}
function private_socket() {
this.on('open', function(client) {
client.send({ hello: client.user.name });
});
}
function room() {
var self = this;
self.on('open', function(client) {
client.send({ room: self.params.name });
});
}
function secure() {
this.on('open', function(client) {
client.send({ welcome: true });
});
}
function where($) {
$.json({ url: $.hostname('/bin/') });
}
function csrf($) {
$.json({ csrf: $.csrf() });
}
```
## Minify and merge CSS, JavaScript and HTML
Source: https://forge.totaljs.es/en/total5/minification/
Framework: total5@0.0.19
Content-SHA256: 194c1898038bdd990369cbcb263bcd01f76642eea7059467e5d1544e067099d9
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js minifies what it serves without any build step. A `.css` or `.js` file from `public/` is sent with comments and whitespace removed (the code is not renamed or restructured, only compacted), and a string sent with `$.html()` or a rendered view has its HTML comments removed and repeated spaces collapsed. A file whose name contains `.min.` (such as `library.min.js`) is considered minified already and is sent as it is.
Three settings in the `config` file control it, all `true` by default: `$minifyjs`, `$minifycss` and `$minifyhtml`. Set one to `false` to serve that type untouched, which is handy when you debug in the browser.
`MERGE(url, file1, file2, ...)` registers a virtual file that is the concatenation of the given files, minified. The browser makes one request instead of many. File paths are URLs of the `public/` folder.
```css public/css/app.css
/* the page style */
body {
margin: 0;
color: #000000;
}
```
```js public/js/app.js
// greeting
function hello(name) {
var message = 'Hello ' + name;
return message;
}
```
```js public/js/library.min.js
function untouched ( ) { return 1 }
```
```js public/js/two.js
function two() { return 2; }
```
```js controllers/minify.js
exports.install = function() {
ROUTE('GET /page/', page);
MERGE('/all.js', '/js/app.js', '/js/two.js');
};
function page($) {
$.html('\n\n \n \n Hello world
\n \n');
}
```
## Resize and convert images
Source: https://forge.totaljs.es/en/total5/images/
Framework: total5@0.0.19
Content-SHA256: 59df6959402e9050bed76d06e7696a95cec76f8421f71a17fc7bff28bef220a9
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js processes images by calling **GraphicsMagick** (`gm`) or ImageMagick, which must be installed on the server (`brew install graphicsmagick`, `apt install graphicsmagick`). The `$imageconverter` setting chooses the program: `gm` (default), `im` or `magick`.
`Image.load(filename)` (or a `Buffer` or a stream) returns a chain that records the operations and runs them when you call `.save(filename, callback)`. The useful operations are `resize(width, height)` (keeps the proportions inside the box), `thumbnail(width, height)` (a fast, small preview), `miniature(width, height)` (fits the image in the box and fills the rest with the background), `crop(width, height, x, y)`, `rotate(degrees)`, `grayscale()`, `blur(radius)`, `quality(percentage)` and `output('jpg')` to convert the format. Uploaded files do it directly: `file.image()` in a handler for `@upload` routes returns the same chain.
Reading the size does not need GraphicsMagick: `Image.measure('png', buffer)` returns `{ width, height }` from the first bytes of PNG, JPG, GIF and WebP data.
`$.image(options)` is for a route that creates a picture on demand: `ext` is the format, `load(next)` hands over the source (a stream or a path) with `next(...)`, `image(img)` describes the operations and `cache` is a name under which the result is kept in `tmp/` for the next requests.
```js controllers/images.js
const Zlib = require('zlib');
const Fs = require('fs');
exports.install = function() {
ROUTE('GET /resize/', resize);
ROUTE('GET /thumb/', thumb);
};
// Builds a solid red PNG so that the example needs no file
function makepng(width, height) {
function chunk(type, data) {
var length = Buffer.alloc(4);
length.writeUInt32BE(data.length);
var body = Buffer.concat([Buffer.from(type), data]);
var crc = Buffer.alloc(4);
crc.writeUInt32BE(Zlib.crc32(body));
return Buffer.concat([length, body, crc]);
}
var header = Buffer.alloc(13);
header.writeUInt32BE(width, 0);
header.writeUInt32BE(height, 4);
header[8] = 8; // bit depth
header[9] = 2; // RGB
var row = Buffer.alloc(1 + width * 3);
for (var x = 0; x < width; x++)
row[1 + x * 3] = 255;
var raw = Buffer.concat(Array(height).fill(row));
return Buffer.concat([
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
chunk('IHDR', header),
chunk('IDAT', Zlib.deflateSync(raw)),
chunk('IEND', Buffer.alloc(0))
]);
}
function source() {
var filename = PATH.tmp('source.png');
Fs.writeFileSync(filename, makepng(200, 100));
return filename;
}
function resize($) {
var target = PATH.tmp('small.jpg');
Image.load(source()).resize(50, 50).output('jpg').save(target, function(err) {
if (err) {
$.invalid(err);
return;
}
var size = Image.measure('jpg', Fs.readFileSync(target));
$.json({ width: size.width, height: size.height, original: Image.measure('png', Fs.readFileSync(source())) });
});
}
function thumb($) {
$.image({
ext: 'png',
cache: 'thumb-example',
load: function(next) {
next(Fs.createReadStream(source()));
},
image: function(img) {
img.resize(40, 40);
}
});
}
```
## Verify numeric schema boundaries instead of assuming integer coercion
Source: https://forge.totaljs.es/en/total5/numeric-schema-boundaries/
Framework: total5@0.0.19
Content-SHA256: 164490677c2f9a2f3852886b573a46742243ade192a31b09fb8cad9656e73591
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
In `total5@0.0.19`, the compact numeric types all enter the native number validator. **`Int` does not guarantee an integer**: `7.9` is preserved. `Number2` also preserves this fraction; do not interpret its name as rounding to two decimal places. Required numeric zero is accepted.
`TinyInt` accepts values from 0 through 255. `SmallInt` accepts -32767 through 32767; notably, -32768 is rejected in this version. These subtype checks enforce bounds but do not require integral values: `TinyInt` accepts `1.5`. Apply an explicit `Number.isInteger()` check for identifiers, counters or quantities that must be integers. These observations describe this version and the tested values, not SQL column semantics.
```js controllers/numbers.js
var schema = NEWSCHEMA('NumericBoundaries', '*integer:Int,*small:SmallInt,*tiny:TinyInt,*decimal:Number2');
exports.install = function() {
ROUTE('POST /numbers/ @json', numbers);
};
function numbers($) {
var result = schema.transform($.body);
if (result.error) {
$.response.status = 400;
$.json({ error: 'invalid numeric input' });
return;
}
$.json(result.response);
}
```
## Use the built-in string, number, date and array helpers
Source: https://forge.totaljs.es/en/total5/utilities/
Framework: total5@0.0.19
Content-SHA256: d746997c3cbc0c0a1fa153c1794c7f9bb910cc42ba745b6782ca68e08e6f57ea
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js adds a set of methods to the JavaScript built-ins. They are available everywhere in the app, so they are worth knowing before you write a helper of your own. The ones below are exercised by the example.
**String:** `slug([max])` makes a URL-safe text (lowercase, no accents or symbols); `format(a, b)` replaces `{0}`, `{1}`; `encode()` / `decode()` escape and restore HTML; `padLeft(length, char)` / `padRight()`; `parseInt2()` and `parseFloat2()` read a number out of a text and return `0` when there is none; `isEmail()`, `isURL()` and `isJSON()` validate; `parseJSON()` parses JSON and returns `null` on error; `md5()`, `sha1()` and `sha256()` return hex hashes; `capitalize()` upper-cases the first letter of every word.
**Number:** `format(decimals, thousands, decimal)`, for example `(1234567.891).format(2)` gives `1 234 567.89`.
**Date:** `format('yyyy-MM-dd HH:mm')`, `add('1 day')` and `diff(date, 'days')`.
**Array:** `take(n)` and `skip(n)`, `first()` and `last()`, `trim()` (trims the texts and removes the empty ones), `findItem(field, value)` and `findIndex(field, value)`, `remove(value)` (removes every match and returns the array), `group(field)` (returns `[{ name, items }]`) and `quicksort(field)`.
**Objects:** `U.extend(target, source)` merges `source` into `target` and returns it; `U.clone(object)` makes a deep copy.
```js controllers/utilities.js
exports.install = function() {
ROUTE('GET /utils/', utils);
};
function utils($) {
var items = [{ type: 'a', n: 1 }, { type: 'b', n: 2 }, { type: 'a', n: 3 }];
var original = { a: { b: 1 } };
var copy = U.clone(original);
copy.a.b = 2;
$.json({
slug: 'Hello Wörld & Co!'.slug(),
slugmax: 'Hello brave new world'.slug(11),
format: '{0} has {1} items'.format('Ann', 3),
encode: '&'.encode(),
decode: '<b>'.decode(),
pad: ['7'.padLeft(3, '0'), '7'.padRight(3, '0')],
numbers: ['abc12x'.parseInt2(), '3,5kg'.parseFloat2(), 'none'.parseInt2()],
is: ['a@b.cz'.isEmail(), 'nope'.isEmail(), 'https://totaljs.com'.isURL(), '{"a":1}'.isJSON(), 'abc'.isJSON()],
json: ['{"a":1}'.parseJSON(), 'not json'.parseJSON()],
md5: 'abc'.md5(),
sha1: 'abc'.sha1(),
capitalize: 'hello big world'.capitalize(),
number: [(1234567.891).format(2), (1234567.891).format(2, ' ', ',')],
date: [new Date(2026, 0, 5, 9, 7).format('yyyy-MM-dd HH:mm'), new Date(2026, 0, 31).add('1 day').format('yyyy-MM-dd'), new Date(2026, 0, 5).diff(new Date(2026, 0, 1), 'days')],
take: [[1, 2, 3, 4, 5].take(2), [1, 2, 3, 4, 5].skip(3), [1, 2, 3].first(), [1, 2, 3].last()],
trim: ['a', '', ' b '].trim(),
find: [[{ id: 1 }, { id: 2 }].findItem('id', 2), [{ id: 1 }, { id: 2 }].findIndex('id', 2)],
remove: [1, 2, 3, 2].remove(2),
group: items.group('type'),
sort: [{ n: 'b' }, { n: 'a' }].quicksort('n'),
extend: U.extend({ a: 1 }, { b: 2 }),
clone: [original.a.b, copy.a.b]
});
}
```
## Record action audit events with a native audit sink
Source: https://forge.totaljs.es/en/total5/action-audit/
Framework: total5@0.0.19
Content-SHA256: b949a031d439b733931dd095add9923cac6304897ab4528084ca59651f89a176
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
An action can set `audit` to a text template. On successful completion, the framework sends an audit record to `DEF.onAudit(name, data, context)`. The automatic record uses the sink name `actions` and includes the action id. An action can also call `$.audit(message, type)` explicitly; this emits its own record and can carry a type such as `info`.
The example replaces the audit sink with an in-memory collector to inspect its contract. It records only the name, action id, message and type. The framework also prepares contextual data and serialized request data: a real sink needs an explicit policy for secrets, retention, access and failures. An audit event is not evidence that a business transaction committed; the action should call its success callback only after the intended write succeeds.
In this version the automatic audit event occurs after the successful action callback path. It does not automatically log validation failures or failed actions.
```js definitions/audit.js
MAIN.auditEvents = [];
DEF.onAudit = function(name, data) {
MAIN.auditEvents.push({ sink: name || null, action: data.action, message: data.message || '', type: data.type || null });
};
NEWACTION('AuditExample|save', {
input: '*name:String',
audit: 'Saved the example',
action: function($) {
$.callback({ ok: true });
}
});
NEWACTION('AuditExample|explicit', {
action: function($) {
$.audit('Explicit example event', 'info');
$.success();
}
});
NEWACTION('AuditExample|fail', {
audit: 'This must not become a success event',
action: function($) {
$.invalid(409);
}
});
```
```js controllers/audit.js
exports.install = function() {
ROUTE('POST /save/ --> *AuditExample|save');
ROUTE('POST /explicit/ --> *AuditExample|explicit');
ROUTE('POST /fail/ --> *AuditExample|fail');
ROUTE('GET /events/', function($) { $.json(MAIN.auditEvents); });
};
```
## Run in debug and release mode
Source: https://forge.totaljs.es/en/total5/release-mode-and-debugging/
Framework: total5@0.0.19
Content-SHA256: 6b9c467dade4b69e103b12fa2687c66dae398141f5b622497f90a81bba6e01b9
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js runs in one of two modes, **debug** for development and **release** for the server, and the global boolean `DEBUG` tells your code which one is active. In debug mode the app runs under a file watcher that restarts it when you change a file, errors show details and source maps are generated. Release mode has no watcher and error pages without the technical details.
The mode is chosen by how the app is started, in the start script (`index.js`) that calls `Total.run(options)`:
- the default is debug, with the watcher;
- `options.release = true` starts release mode; the start script that Total.js generates sets it from the command line, so `node index.js --release` is the way to start production;
- `options.watcher = false` runs a single process without the watcher, and that is always release mode (it is what the examples of this cookbook use).
Before `Total.run()` has decided, `DEBUG` has a default taken from `NODE_ENV`: it is `false` when `NODE_ENV=production` and `true` otherwise. Code that runs at the moment a module is loaded sees that default, so do not rely on `NODE_ENV` for the final mode, only on `DEBUG` after the app has started.
The routes can follow the mode. The flag `@debug` makes a route exist only in debug mode and `@release` only in release mode; in the other mode the URL answers 404. Use `@debug` for tools you need while developing (a database viewer, a seed route) so they can never reach production by accident.
```js controllers/mode.js
exports.install = function() {
ROUTE('GET /mode/', mode);
ROUTE('GET /tools/ @debug', tools);
ROUTE('GET /status/ @release', status);
};
function mode($) {
$.json({ debug: DEBUG });
}
function tools($) {
$.json({ tools: true });
}
function status($) {
$.json({ status: 'ok' });
}
```
## Test the application with the built-in test runner
Source: https://forge.totaljs.es/en/total5/tests/
Framework: total5@0.0.19
Content-SHA256: 86c51b7edfa17059a77abaa717c02fc9a5330e98f06d50fca0798bcb7ec815df
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Total.js has a small test runner, `total5/test`. A test file is a normal Total.js script: it loads the framework and the runner, starts the application and, when it is `ready`, registers its tests. `Test.push(name, fn)` adds a test; `fn(next)` does its work and calls `next()` to continue with the following one. `Test.print(message, error)` reports one check: pass `null` as `error` for success (`[OK]` is printed) or a message for failure (`[FAIL]` is printed and the process exits with code 1). `Test.run()` runs the registered tests in order and exits with code 0 at the end.
Because failure ends the process with a non-zero code, the file can be called by `npm test` and by any CI service. Wait a moment (about 500 ms) after `ready` before the first request: a request sent right away can still get a 404. Use `RESTBuilder` (or `REQUEST`) against the local server to test routes, and call actions directly with `ACTION()`.
```js controllers/sum.js
exports.install = function() {
ROUTE('GET /sum/{a}/{b}/', sum);
ROUTE('GET /dir/', dir);
};
function sum($) {
$.json({ total: +$.params.a + +$.params.b });
}
function dir($) {
$.json({ dir: PATH.root() });
}
```
```js test.js
require('total5');
require('total5/test');
// Another port than the application uses, so both can run
Total.run({ port: 8765, release: false, watcher: false });
ON('ready', function() {
// A request sent in the same tick as "ready" can still get a 404, so give the routes a moment
setTimeout(runtests, 500);
});
function runtests() {
Test.push('The sum route', function(next) {
RESTBuilder.GET('http://127.0.0.1:8765/sum/2/3/').exec(function(err, response) {
Test.print('2 + 3 = 5', response && response.total === 5 ? null : 'Expected 5, got ' + JSON.stringify(response));
next();
});
});
Test.push('Unknown route', function(next) {
RESTBuilder.GET('http://127.0.0.1:8765/missing/').exec(function(err, response, output) {
Test.print('/missing/ answers 404', output.status === 404 ? null : 'Expected 404, got ' + output.status);
next();
});
});
Test.run();
}
```
## Use all CPU cores with the cluster mode
Source: https://forge.totaljs.es/en/total5/cluster/
Framework: total5@0.0.19
Content-SHA256: 99d3248ff978a66dc94b11b8e9618643225d23dc06e9d6f4278b01bbb9f41e09
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A Node.js process uses one CPU core. `Total.run({ cluster: N })` starts a **master** that forks `N` worker processes, each running your whole application, and the operating system shares the incoming connections between them (`'auto'` instead of a number lets the master add threads as the load grows). Run it in release mode (`release: true`, which the generated start script sets with `--release`); the debug watcher is not meant for a cluster. In the start script it is one option:
```js
Total.run({ cluster: 4, release: true }); // or cluster: 'auto'
```
Every worker is a separate process with its own memory, so anything kept in a variable, in `CACHE` or in `MEMORIZE` exists once **per worker** and is not shared: two requests of the same user can land on different workers. Keep shared state in a database, in a file storage or in a message service, and keep sessions in a store all workers can read. Scheduled work such as `CRON()` also runs in every worker, so guard it if it must run once.
**Check that all the workers are up.** After a deploy, request a URL like `/pid/` below repeatedly and count the distinct ids: you should see as many as the workers you asked for.
Each worker has its own process id. The example starts two workers and includes that id in the response.
```js controllers/pid.js
exports.install = function() {
ROUTE('GET /pid/', pid);
ROUTE('GET /dir/', dir);
};
function pid($) {
$.json({ pid: process.pid });
}
function dir($) {
$.json({ dir: PATH.root() });
}
```
```js cluster.js
require('total5');
Total.run({ cluster: 2, port: 8766, release: true });
```
## Load a JavaScript module from a URL with IMPORT()
Source: https://forge.totaljs.es/en/total5/import-remote-module/
Framework: total5@0.0.19
Content-SHA256: d9ee8c3000dc6dc3cbcd81c5a4523bfb6863dca95d50aafed96c8f1755a91c0a
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`IMPORT(url, [callback])` downloads a JavaScript file, saves it in the `tmp` folder of the application and returns what the file exports (`module.exports`), as `require()` would. With a callback it is `callback(err, module, response)`; without it, `IMPORT()` returns a promise for the module. The address is remembered: calling `IMPORT()` again with the same address does **not** download the file again, it requires the one in `tmp` (and the file stays there between restarts, but the memory of "already downloaded" does not: after a restart it is downloaded again).
**This runs code that comes from the network with all the rights of your application.** Import only from an address that you control and that you reach with `https`, never one that a user sent you or that anyone else can change. The file is a CommonJS module. **The status of the answer is not checked:** an address that answers `404` (or any error page) is saved as if it were the module, and `require()` then throws a `SyntaxError` inside the download, so in `total5` 0.0.19 the callback is **never called** and the promise never settles. Always give `IMPORT()` a limit of time of your own, as the last route does, and check the address before you depend on it.
Here the file comes from a route of the same application, so that the example needs no network. The route counts the downloads.
```js controllers/import.js
exports.install = function() {
ROUTE('GET /lib/tax/', lib);
ROUTE('POST /use/', use);
ROUTE('POST /use-twice/', use_twice);
ROUTE('POST /missing/', missing);
ROUTE('GET /downloads/', downloads);
};
let count = 0;
function lib($) {
count++;
$.plain('module.exports = { vat: function(price) { return Math.round(price * 121) / 100; } };');
}
function downloads($) {
$.json({ downloads: count });
}
function base() {
return 'http://127.0.0.1:' + CONF.$port;
}
async function use($) {
const tax = await IMPORT(base() + '/lib/tax/');
$.json({ price: tax.vat(100) });
}
function use_twice($) {
IMPORT(base() + '/lib/tax/', function(err, tax) {
$.json({ err: err ? err.toString() : null, price: tax ? tax.vat(200) : null });
});
}
async function missing($) {
// the address does not exist: the answer is the 404 page of the application
const winner = await Promise.race([
IMPORT(base() + '/lib/missing/').then(() => 'answered', () => 'answered'),
new Promise(resolve => setTimeout(() => resolve('no answer'), 1500))
]);
$.json({ result: winner });
}
```
## Build a flow of components with Flow.load() and run it with exec()
Source: https://forge.totaljs.es/en/total5/flowstream/
Framework: total5@0.0.19
Content-SHA256: 8bb7bd513b4f12b9d58eafbd976efdc76a26cc9b10ead6bb184f67b0e7af38c6
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A **FlowStream** is a small program made of **components** (boxes with inputs and outputs) and **connections** between them. Total.js runs it inside your application: a message enters one component, the component sends results to its outputs, and the connections carry them to the next components. The same schema is what the Flow designer of Total.js draws; here it is written by hand.
`Flow.load(schema, [callback])` starts a FlowStream and gives the callback `(err, instance)`. The schema is an object:
- `id` and `name`: the id is the key in `Flow.instances` (`Flow.instances.demo`); without an id a random one is made.
- `components`: an object with the **source code** of each component, as text. The text is a script, `` (the form that the designer saves).
- `design`: the instances of the components. Each one has an `id`, the `component` that it uses, its `config` and `connections`, an object whose keys are **output ids** and whose values are lists of `{ id, index }`: the instance and the **input id** that receive what is sent to that output.
- `variables`: values that components can read.
A component is a module: `exports.id`, `exports.name`, `exports.inputs` and `exports.outputs` (lists of `{ id, name }`), `exports.config` (the defaults) and `exports.make = function(instance, config)`. In `make()` the component sets `instance.message = function($) {}`, which receives each message: `$.data` is the data, `$.send(outputid, data)` sends it to an output and `$.destroy()` ends a message that does not go any further.
To use the flow from your code, `instance.exec({ id, data, callback })` sends `data` to the instance `id` and calls `callback(err, message)` when the message ends; the last message of the chain is `message` (so `message.data` is what the last component received). `instance.reconfigure(id, config)` changes the configuration of a running instance and `Flow.remove(id)` stops the FlowStream and forgets it. `FlowStream` is the same object as `Flow`: two names for the manager.
**A component is code that runs with all the rights of the application** (in the same thread unless the schema says `worker`). Never load a schema or a component that a visitor can write.
```js definitions/flow.js
const upper = ``;
const collect = ``;
MAIN.collected = [];
MAIN.flow = null;
Flow.load({
id: 'demo',
name: 'Demo',
components: { upper: upper, collect: collect },
design: {
a: { id: 'a', component: 'upper', config: { prefix: '> ' }, connections: { output: [{ id: 'b', index: 'input' }] } },
b: { id: 'b', component: 'collect', config: {}, connections: {} }
},
variables: {}
}, function(err, instance) {
MAIN.flow = instance;
});
```
```js controllers/flow.js
exports.install = function() {
ROUTE('GET /shout/', shout);
ROUTE('POST /prefix/ @json', prefix);
ROUTE('GET /collected/', collected);
ROUTE('GET /info/', info);
ROUTE('POST /stop/', stop);
};
function shout($) {
if (!MAIN.flow)
return $.invalid('The flow is not running');
MAIN.flow.exec({ id: 'a', data: $.query.text, callback: (err, message) => $.json({ result: message ? message.data : null }) });
}
function prefix($) {
MAIN.flow.reconfigure('a', { prefix: $.body.prefix });
$.json({ ok: true });
}
function collected($) {
$.json(MAIN.collected);
}
function info($) {
$.json({ flows: Object.keys(Flow.instances), instances: Object.keys(MAIN.flow.flow.meta.flow), same: Flow === FlowStream });
}
function stop($) {
Flow.remove('demo');
MAIN.flow = null;
$.json({ flows: Object.keys(Flow.instances) });
}
```
## Test a Flow component alone with Flow.test()
Source: https://forge.totaljs.es/en/total5/flowstream-component-test/
Framework: total5@0.0.19
Content-SHA256: c9944def331db1296e725f6200d7bcf3bf60c1265eed77a225d7c71d3864d64e
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`Flow.test(name, source, [config], [init])` builds a flow with **one** component (the `source` is its code, like in the recipe of FlowStream, and `config` replaces its configuration) and a dummy component that collects its outputs. `init(test)` is called when the flow is ready, with the object `test`:
- `test.send(inputid, data)` (also `test.input`) sends a message to an input of the component and returns it.
- `test.message = function($, data)`: called with every message that the component sends to **any of its outputs**; `$` is the message and `data` its data.
- `test.reconfigure(config)` changes the configuration; `test.configure = function(config)` is called after it. `test.variables(object)` sets the variables of the flow.
- `test.assert(ok, message)`, `test.ok(message)` and `test.fail(message)` print `[OK]` or `[NO]` with the date, and a failure throws. The functions are plain properties: replace `test.ok` and `test.fail` to send the result somewhere else, as the example does.
- The test flow always has the id `test`: only one `Flow.test()` at a time.
It is meant for the tests of a component that is written for a Flow, run with `node index.js` in a small application of its own.
```js definitions/component-test.js
MAIN.results = [];
const source = ``;
ON('ready', function() {
Flow.test('tax', source, { rate: 10 }, function(test) {
test.ok = message => MAIN.results.push('ok: ' + message);
test.fail = message => MAIN.results.push('no: ' + message);
test.message = function($, data) {
test.assert(data.net === 100, 'the net amount comes back');
test.assert(data.total === 110, 'the configuration of the test (10 %) is used, got ' + data.total);
MAIN.results.push('message: ' + JSON.stringify(data));
};
test.send('input', 100);
});
});
```
```js controllers/results.js
exports.install = function() {
ROUTE('GET /results/', results);
};
function results($) {
$.json(MAIN.results);
}
```
## Call a Python program that keeps running with PYPELINE()
Source: https://forge.totaljs.es/en/total5/pypeline/
Framework: total5@0.0.19
Content-SHA256: c346fc22c80a08961b4ed4e5da21cc650cb0f3fac63fddec6f03504e6f5bcaf7
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`PYPELINE(name, [options])` starts `pypelines/.py` with `python3` and keeps a connection to it, so the application can use Python libraries (data, models, image processing) without starting a new process for every request. It returns an object with `send(data)` to send a message, `on('data', fn)` to receive the answers and `close()` to stop the script. A name that starts with `~` is the path of a file instead (`~/opt/job.py`), and a text that is code (it has line breaks or looks like Python) is run as an inline program with `python3 -c`.
- **The protocol.** The script is started as `python3 -u script.py `: the **last argument is the path of a Unix socket** that the application listens on, and the script connects to it. Every message, in both directions, is a frame: 4 bytes with the length (big endian) followed by the body, which is JSON by default.
- **Options:** `type` (`'json'` default, `'text'`, or anything else for raw `Buffer`s), `autorestart` (`false` to not restart the script when it ends; by default it is started again), `threads` and `socket: false` (no socket, only start the program).
- Messages sent before the script has connected wait and are delivered when it does.
- Events: `open` (the script connected), `data` (a message), `restart` and `close`.
- **The path of the socket has a limit.** The socket is a file in the `tmp` folder of the application and the system limits the whole path (about 104 characters on macOS and 108 on Linux). In a project that is deep in the disk the server fails with `listen EINVAL`, the script never connects and the messages wait forever. The path is the property `pypeline.socket`: set it right after `PYPELINE()` (the file is checked and the server started afterwards) to a short one, as the example does.
- The Python program is a **normal program that runs with the rights of the application**: it is only as safe as the code of the file, and Python 3 has to be installed (`python3` in the `PATH`).
```python pypelines/shout.py
import json
import socket
import struct
import sys
# The last argument is the socket that the application listens on
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(sys.argv[-1])
def read(count):
data = b''
while len(data) < count:
chunk = sock.recv(count - len(data))
if not chunk:
raise SystemExit(0)
data += chunk
return data
while True:
size = struct.unpack('>I', read(4))[0]
message = json.loads(read(size).decode('utf-8'))
answer = json.dumps({ 'id': message['id'], 'upper': message['text'].upper(), 'words': len(message['text'].split()) }).encode('utf-8')
sock.sendall(struct.pack('>I', len(answer)) + answer)
```
```js controllers/py.js
exports.install = function() {
ROUTE('POST /shout/ @json', shout);
};
const pypeline = PYPELINE('shout');
// A short path for the socket: the folder of a project can be too deep for a Unix socket
pypeline.socket = require('os').tmpdir() + '/forge-shout-' + process.pid + '.socket';
const pending = {};
let counter = 0;
// Every answer says which message it is for
pypeline.on('data', function(answer) {
const callback = pending[answer.id];
if (callback) {
delete pending[answer.id];
callback(answer);
}
});
function shout($) {
const id = ++counter;
const timer = setTimeout(() => $.invalid('The script did not answer'), 5000);
pending[id] = function(answer) {
clearTimeout(timer);
$.json(answer);
};
pypeline.send({ id: id, text: $.body.text });
}
```
## Enums and string-format validation boundaries
Source: https://forge.totaljs.es/en/total5/schema-enums-and-formats/
Framework: total5@0.0.19
Content-SHA256: 7170c1adf5f76b4f2a6754b93b35b731172e178c0fabc248d6e3043eadc8a83c
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Compact enums use `status:{draft|published}` (semicolon also separates choices). The string is trimmed before exact, case-sensitive membership checking. A required enum rejects an omitted/empty value. Nested objects use `{name:Type}`; they are not enum declarations.
In `total5@0.0.19`, `Color` accepts hexadecimal `#RGB`, `#RRGGBB` and `#RRGGBBAA`; it does not accept CSS color names. `Icon` validates a class-name pattern, not the presence of the icon asset. `Base64` and `Zip` use the framework's validators: format acceptance is not content verification. In the compact-schema parser of this version, **`JSON` and `IP` are not recognized subtypes and fall back to plain strings**. Malformed JSON and invalid IP text are accepted. The explicit JSON Schema below sets native `subtype` values to enable these checks; it does not parse a JSON string into an object. Do not use `SafeString` in place of bound SQL parameters or output escaping.
```js controllers/formats.js
var schema = NEWSCHEMA('FormatBoundaries', '*status:{draft|published},*color:Color,*blob:Base64,*zip:Zip,*icon:Icon,*document:JSON,*address:IP');
var explicit = Total.newjsonschema('ExplicitFormats', { $id: 'ExplicitFormats', type: 'object', required: ['document','address'], properties: { document: { type: 'string', subtype: 'json' }, address: { type: 'string', subtype: 'ip' } } });
exports.install = function() {
ROUTE('POST /explicit/ @json', function($) {
var result = explicit.transform($.body);
if (result.error) { $.response.status = 400; $.json({ error: 'invalid format' }); }
else $.json(result.response);
});
ROUTE('POST /formats/ @json', formats);
};
function formats($) {
var result = schema.transform($.body);
if (result.error) {
$.response.status = 400;
$.json({ error: 'invalid format' });
} else
$.json(result.response);
}
```
## TotalAPI and Componentator service boundaries
Source: https://forge.totaljs.es/en/total5/remote-service-boundaries/
Framework: total5@0.0.19
Content-SHA256: 5524e132d24137a2f048bb67797b5f5cbee93beb1b855059c50d44c081d0e2d4
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`TotalAPI(operation, data)` delegates to `API('TAPI', operation, data)`. The built-in driver sends requests to the regional endpoint selected by `CONF.$tapiurl`, using a configured token such as `CONF.secret_totalapi`. `.promise()` and `.callback()` use the same API builder as other drivers. With `$tapi:false`, operations other than the driver's special `check` operation fail locally with `totalapi_inactive`.
`COMPONENTATOR(name, components, removeprev=true, attrs='')` determines a public bundle filename, stores its URL in `REPO[name]` and fetches it from Componentator only when the file is absent. With `removeprev:true`, older matching files may be deleted. The `componentator` event receives the name/component metadata before the lookup. This function returns before its async filesystem/download work has finished; the repo URL alone is not proof of download success.
For locally vendored frontend assets, serving known files avoids dependence on remote bundle availability. For live TotalAPI/Componentator integration, separately verify your exact service, credentials, network behavior and returned content. These services are optional and are not dependencies needed to start a native Total.js application.
```text config
$tapi : false
```
```js controllers/remote.js
exports.install = function() {
ROUTE('GET /disabled-api/', async function($) {
try { await TotalAPI('forge-fixture-no-service', {}).promise(); $.json({ unexpected: true }); }
catch (e) { $.json({ error: String(e) }); }
});
ROUTE('GET /cached-componentator/', function($) {
var components = 'forge-local-fixture';
var relative = 'ui-' + components.makeid() + '.min.js';
F.Fs.writeFileSync(PATH.public(relative), '/* local fixture */');
COMPONENTATOR('fixture', components, false);
$.json({ url: REPO.fixture, expected: '/ui-' + components.makeid() + '.min.js' });
});
};
```
## QueryBuilder operations, filters and driver contract
Source: https://forge.totaljs.es/en/total5/query-builder-contract/
Framework: total5@0.0.19
Content-SHA256: f155d117230e8d40e4895b685b1dec15b996d5a867b5d917fe2972d66bac8e8e
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
This page covers the native `total5@0.0.19` QueryBuilder (`querybuilder.js`). `DB()` returns a queue controller. Each operation schedules a driver call; append filters and callbacks synchronously before it executes. `NEWDB(name, driver)` registers a connection. `NEWDB(name)` unregisters it. There is no implicit database engine or transaction in this queue.
The example driver returns the received options. To execute a database operation, implement that operation in the driver. For actual SQL execution and rollback see [query-builder-sqlite](/en/total5/query-builder-sqlite/).
| Operations | Driver receives |
|---|---|
| `find/all(table)`, `read/one(table)`, `list(table)` | `exec`, `table`; `read` sets `first:true`, `take:1`; `list` defaults to `take:100`, `skip:0` |
| `check(table)`, `count(table)` | `check` sets `first:1`, `take:1`; the driver defines the result value |
| `insert/ins(table, data)` | `payload` |
| `update/modify/mod/upd(table, data, upsert)` | `payload`, `upsert`; a zero result triggers a second `insert` when upsert is enabled |
| `remove/rem(table)`, `truncate/clear(table)`, `drop(table)` | The corresponding `exec`; destructive operations need driver support |
| `scalar(table, type, key, key2)` | `scalar:{type,key,key2}`; omitted `key` becomes `*` |
| `command(table, name)` | `command:name` |
| `query(sql, params)` or `query(connection, sql, params)` | Raw text in `query`, values in `params`, table is empty; SQL binding is the driver's responsibility |
| Query methods | Options / behavior |
|---|---|
| `where(name, [comparer], value)`, `equal(object)`, `id(value)`, `userid(value)` | Equality shorthand; `==` becomes `=`, `!=` becomes `<>`; array IDs use `in` |
| `in/notin(name, values, [idField])`, `between(name,a,b)`, `or(fn)` | Filter records; `or` groups the records added inside `fn(builder)` |
| `array(name,[comparer],value)` | Default comparer `&&`; interpretation belongs to driver |
| `month/day/year/hour/minute(name,[comparer],value)` | Date-part filters; default comparer `=` |
| `search(name,value,[where])`, `contains(name)`, `empty(name)` | Filter records; matching semantics belong to driver |
| `query(fragment)`, `join(name,table,type,a,b)`, `permit(name,type,values,userid,required)` | Driver-specific filters; raw fragments must come from trusted application code |
| `fields(csv)`, `returning(csv)` | Arrays of trimmed field names, not SQL escaping or authorization |
| `sort(name, descending)`, `gridsort(csv,[localized])` | Sort strings; `true` or `desc` select descending |
| `take/limit(n)`, `skip(n)`, `page(page,limit)`, `paginate(page,limit,max)` | Offsets/limits; `paginate` caps the limit, `page` does not |
| `first()`, `primarykey(name)`, `schema(name)`, `language(value,prefix,skip)`, `debug()` | Metadata for driver; `first` also unwraps an array result |
| `gridfilter(name,query,type,[key])`, `autoquery(query,schema,sort,max)` | Convert grid query parameters into options. `autoquery` restricts requested fields and sort keys to its declared schema |
| `gridfields(fields,allowed)` | Use `fields()` with your own allowlist: it is the tested way to choose the columns |
| `callback/pipe(fn)`, `promise()`, `done($,fn,param)` | Completion; promises reject on driver error |
| `data(fn)`, `fail(fn)`, `error/err(message,reverse)` | Success/failure hooks; `error()` rejects empty results, `reverse:true` rejects truthy results (including an empty array) |
| `set(name)`, `nobind()` | Name the queued result, or do not replace queue output |
| `insert(fn,param)` on an update builder | Mutate the payload before the fallback insert of an upsert |
| `audit($,message,type)` | Calls the supplied context's `variables()` and `audit()` after a successful driver response |
Controller methods: `callback/pipe`, `promise`, `done`, `debug`, `language`, `load(connection, options)` and `exec(options, callback)`. `load` copies a defined subset of serialized options; `exec` dispatches supported operation names. `next/free` are lifecycle internals. A queue stops after a driver error; it does not undo earlier writes.
```js definitions/contract.js
NEWDB('contract', function(options, callback) {
if (options.table === 'failure')
return callback(new Error('driver failed'));
if (options.table === 'empty')
return callback(null, []);
if (options.table === 'upsert') {
MAIN.upsert.push(JSON.parse(JSON.stringify(options)));
return callback(null, options.exec === 'update' ? 0 : options.payload);
}
if (options.exec === 'list')
return callback(null, { items: [], count: 12 });
callback(null, options.first ? [options] : options);
});
MAIN.upsert = [];
```
```js controllers/contract.js
exports.install = function() {
ROUTE('GET /contract/', contract);
};
async function contract($) {
try {
var out = {};
var methods = ['find', 'all', 'read', 'one', 'list', 'check', 'count', 'insert', 'ins', 'update', 'modify', 'mod', 'upd', 'remove', 'rem', 'truncate', 'clear', 'drop'];
out.operations = {};
for (var method of methods)
out.operations[method] = await DB()[method]('contract/items', { name: 'Ann' }).promise();
out.scalar = await DB().scalar('contract/items', 'sum', 'amount', 'group').promise();
out.command = await DB().command('contract/items', 'vacuum').promise();
out.raw = await DB().query('contract', 'SELECT ?', [7]).promise();
out.filters = await DB().find('contract/items').id([1, 2]).userid({ id: 'u1' }).equal({ active: true }).where('age', '!=', 0).in('team', [{ id: 'a' }], 'id').notin('team', 'b').between('age', 18, 65).or(function(q) { q.where('role', 'a').where('role', 'b'); }).array('tags', ['x']).month('created', 1).day('created', 2).year('created', '>=', 2020).hour('created', 3).minute('created', 4).search('name', 'ann', 'beg').contains('email').empty('deleted').query('trusted_fragment').join('team', 'teams', 'left', 'a.id', 'b.id').permit('permissions', 'RW', ['team'], 'u1', false).promise();
out.options = await DB().find('contract/items').fields('id, name').returning('id, name').sort('name', true).gridsort('id_asc').limit(5).skip(2).primarykey('uid').schema('public').language({ language: 'es' }, '_').debug().promise();
out.page = await DB().list('contract/items').paginate('2', '500', 10).promise();
out.grid = await DB().find('contract/items').autoquery({ fields: 'id,password', sort: 'password_desc,name_desc', page: '2', limit: '500', age: '>18' }, 'id:String,name:String,age:Number', 'id_asc', 10).promise();
out.gridfields = await DB().find('contract/items').gridfields('id', 'id,name').promise();
var queue = DB();
queue.read('contract/items').set('one');
queue.count('contract/items').set('count');
queue.find('contract/items').nobind();
out.queue = await queue.promise();
var hooks = [];
await DB().read('contract/empty').data(function(v) { hooks.push(v === null ? 'null' : 'value'); }).promise();
try { await DB().read('contract/empty').error('missing').fail(function() { hooks.push('fail'); }).promise(); } catch (e) { out.missing = String(e); }
try { await DB().find('contract/failure').promise(); } catch (e) { out.failure = String(e); }
out.hooks = hooks;
out.upsert = await DB().update('contract/upsert', { name: 'new' }, true).where('id', 7).insert(function(payload, role) { payload.role = role; }, 'member').promise();
out.upsertCalls = MAIN.upsert;
out.load = await DB().load('contract', { table: 'items', exec: 'find', take: 2, filter: [] }).promise();
$.json(out);
} catch (e) { $.invalid(e); }
}
```
## Compile and run a native server component
Source: https://forge.totaljs.es/en/total5/native-components/
Framework: total5@0.0.19
Content-SHA256: 9445f3e45dbdb4d6c027386dcc52d91d2904a31c44893a2f970d9e3ab5324aaf
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`NEWCOMPONENT(html, [callback])` compiles a **server component**, distinct from a browser jComponent. It accepts source text containing `';
var component = await NEWCOMPONENT(source);
component.debugger = false;
var instance = component.create();
var first = await new Promise(resolve => instance.input('input',{name:'Ann'},message => resolve(message.data)));
var invalid = await new Promise(resolve => instance.input('input',{},message => resolve(message.data)));
instance.reconfigure({prefix:'two:'});
var second = await new Promise(resolve => instance.input('input',{name:'Bob'},message => resolve(message.data)));
instance.remove();
var rejected = false;
try { await NEWCOMPONENT('no backend script'); } catch (e) { rejected = true; }
$.json({first:first,invalid:invalid,second:second,remaining:component.instances.length,closed:MAIN.componentClosed,rejected:rejected});
} catch (e) { $.invalid(e); }
}
```
## Controller callbacks, manual authorization and conditional responses
Source: https://forge.totaljs.es/en/total5/controller-completion-and-cache/
Framework: total5@0.0.19
Content-SHA256: 787a7a2bd01ce5f203e2d87e26c6bd63f6e6db685bd180c8648853021e8f2bd0
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
These are ordinary route-controller methods, distinct from the action context. `$.jsonstring()` takes already-serialized JSON. `$.respond()` selects the method named by `$.response.output`, defaulting to JSON. `$.done(true)` forwards the callback result inside the success envelope; another argument selects a fixed value. `$.successful(fn)` calls your function only when there is no error.
`await $.authorize()` resolves with the authorization result; an anonymous result must still be handled by your code. For conditional responses pass a **UTC header string** to `$.notmodified()`: in 0.0.19 comparison is exact string equality. `$.httpcache()` sets headers but `$.json()` overwrites cache policy, so the example sends a raw buffer after setting them. Use public caching only for public representations. `$.custom()` transfers response ownership to your handler; finish the Node response yourself.
```js definitions/auth.js
AUTH(function($) {
if ($.headers['x-demo-user'] === 'alice')
$.success({ id: 'alice' });
else
$.invalid();
});
```
```js controllers/completion.js
exports.install = function() {
ROUTE('GET /serialized/', $ => $.jsonstring('{"ok":true}'));
ROUTE('GET /respond/', function($) { $.response.output = 'plain'; $.respond('hello'); });
ROUTE('GET /done/', $ => $.done(true)(null, { id: 7 }));
ROUTE('GET /successful/', $ => $.successful(value => $.json({ value: value }))(null, 9));
ROUTE('GET /callback-error/', $ => $.successful(() => $.json({ wrong: true }))('failure'));
ROUTE('GET /manual-auth/', async function($) {
var user = await $.authorize();
if (user)
$.json({ id: $.user.id });
else
$.invalid(401);
});
ROUTE('GET /conditional/', function($) {
var modified = 'Mon, 05 Jan 2026 00:00:00 GMT';
if ($.notmodified(modified))
return;
$.httpcache(modified);
$.binary(Buffer.from('public version 1'), 'text/plain');
});
ROUTE('GET /custom/', function($) {
$.custom();
$.res.writeHead(202, { 'content-type': 'text/plain' });
$.res.end('accepted');
});
};
```
## Action transport options and explicit TMS publishing
Source: https://forge.totaljs.es/en/total5/action-transport-boundaries/
Framework: total5@0.0.19
Content-SHA256: 8baa757137eb3c808eb19ae317806f99304c91c1a20c49ff788442cfce32e39c
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
In `total5@0.0.19`: `NEWACTION()` stores `csrf`, `encrypt` and `compress` in action metadata, but they do not establish the HTTP guarantees their names might suggest on HTTP routes:
- `csrf:true` alone accepts a POST without a token. Use a middleware that calls `DEF.onCSRFcheck()` with `secret_csrf` configured, as in [csrf-protection](/en/total5/csrf-protection/).
- `encrypt:true` on a declared route adds `@encrypt` metadata, but this version still accepts plaintext JSON and returns plaintext JSON. Use HTTPS for transport.
- `compress:false` does not disable ordinary HTTP gzip; response compression follows `$httpcompress`, the client's accepted encoding and response size. A large JSON response can be gzipped even when this action option is false.
- `publish` declares a TMS topic/schema. Completing an action does not automatically publish its result. Call `$.publish(value)` explicitly and construct the public payload deliberately. Select the fields to publish before calling `$.publish(value)`.
Configure the TMS token for your application. TMS messages go to subscribers of that topic; application permission and tenancy rules still belong in your code.
```text config
secret_csrf : local-csrf-fixture-only
$tms : true
secret_tms : local-tms-fixture-only
$httpcompress : true
```
```js definitions/transport.js
NEWACTION('Transport|csrf', {
csrf: true,
input: '*value:String',
action: function($, model) { $.callback(model); }
});
NEWACTION('Transport|encrypted', {
encrypt: true,
route: 'POST /encrypted/',
input: '*value:String',
action: function($, model) { $.callback(model); }
});
NEWACTION('Transport|compressed', {
compress: false,
action: function($) { $.callback({ text: 'abcdefghij'.repeat(400) }); }
});
NEWACTION('Transport|notice', {
publish: 'value:String',
input: '*value:String',
action: function($, model) {
$.publish({ value: model.value });
$.success();
}
});
```
```js controllers/transport.js
exports.install = function() {
ROUTE('POST /csrf-option/ @json --> Transport|csrf');
ROUTE('GET /compression-option/ --> Transport|compressed');
ROUTE('POST /notice/ @json --> Transport|notice');
};
```
## Invalidate release-mode static caches with TOUCH
Source: https://forge.totaljs.es/en/total5/touch-static-cache/
Framework: total5@0.0.19
Content-SHA256: 74d53a2b7d1a609a9c88ee5f5d38268ea8eb33b899b80fd78848ba95b5ae4e8c
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`TOUCH(url)` clears three cache entries for that URL: `F.temporary.minified`, `tmp` and `notfound`. `TOUCH()` clears those maps. It does not clear `CACHE`, NoSQL documents or every view/configuration cache. Call it after changing a generated asset in release mode. Keep this operation in trusted server code; the write endpoint below is only a local fixture.
Cache invalidation applies to the running process. When an application runs in several processes, coordinate invalidation across those processes.
```text public/generated.txt
before
```
```js controllers/assets.js
exports.install = function() {
ROUTE('POST /regenerate/', function($) {
F.Fs.writeFileSync(PATH.public('generated.txt'), 'after');
F.Fs.writeFileSync(PATH.public('appeared.txt'), 'created');
TOUCH('/generated.txt');
TOUCH('/appeared.txt');
$.success();
});
};
```
## OPENCLIENT RPC, events and shared connections
Source: https://forge.totaljs.es/en/total5/openclient-protocol/
Framework: total5@0.0.19
Content-SHA256: 3debacfd6c900cdaaedcd3e3d489b7a1403f947f09e98c01bd4c4e42509ccf42
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`OPENCLIENT(url, [id])` is the native Open Platform protocol client. It converts `http`/`https` URLs to `ws`/`wss`. Logical clients with the same URL share a WebSocket. `.online(fn)` receives the server's `{type:'init',id:...}` metadata; `.message(fn)` receives unsolicited messages. `.rpc(message)` returns a promise; `.send(message, callback, [filter], [timeoutMs])` uses a callback; `.cmd(message)` sends without waiting. `.remove()` closes the physical socket only after its last logical client is removed.
The example includes a local protocol server. `callbackid`, `error` and `response` are protocol fields. A callback timeout is opt-in; set one for requests that need a deadline. Keep real platform credentials and URLs in server configuration.
```js controllers/open.js
exports.install = function() {
ROUTE('SOCKET /fixture/', fixture);
ROUTE('GET /openclient/', exercise);
};
function fixture() {
var server = this;
server.on('open', function(client) {
client.send({ type: 'init', id: 'local-fixture', version: 1 });
});
server.on('message', function(client, message) {
if (message.type === 'ignore')
return;
if (message.type === 'event') {
client.send({ type: 'notice', value: message.value });
return;
}
client.send({ callbackid: message.callbackid, error: message.type === 'fail' ? 'denied' : null, response: { value: message.value } });
});
}
async function exercise($) {
var a;
var b;
try {
var url = $.hostname('/fixture/');
a = OPENCLIENT(url, 'first');
await new Promise(function(resolve, reject) {
var timer = setTimeout(() => reject(new Error('init deadline')), 2000);
a.online(function(meta) { if (meta) { clearTimeout(timer); resolve(); } });
});
b = OPENCLIENT(url, 'second');
var shared = a.ws === b.ws;
var sum = await a.rpc({ type: 'echo', value: 7 });
var event = new Promise(resolve => a.message(resolve));
a.cmd({ type: 'event', value: 9 });
var notice = await event;
var denied = await new Promise(resolve => a.send({ type: 'fail' }, err => resolve(String(err)), 300));
var timeout = await new Promise(resolve => a.send({ type: 'ignore' }, err => resolve(String(err)), 50));
a.remove();
a = null;
var alive = await b.rpc({ type: 'echo', value: 11 });
var meta = b.meta;
b.remove();
b = null;
$.json({ shared: shared, sum: sum, notice: notice, denied: denied, timeout: timeout, alive: alive, meta: meta });
} catch (e) { $.invalid(e); }
finally { if (a) a.remove(); if (b) b.remove(); }
}
```
## Load FlowStream files and execute isolated workers
Source: https://forge.totaljs.es/en/total5/flow-files-and-workers/
Framework: total5@0.0.19
Content-SHA256: 141aa8d246eacc9f5b9663819e73f10218ac1ebaff06febc7bd0e8e79a85eb9f
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`Flow.init(directory, callback)` reads `.flow` JSON files and loads each trusted schema; names containing `-bk` or `_bk` are skipped. The default directory is `flowstreams/`. `Flow.load(schema, callback)` also accepts `worker:'worker'` for a Node worker thread and `worker:true` for a child process in this version. Flow workers have separate globals: do not expect the parent's `MAIN` objects or closures to be available inside a component.
The visual Flow designer and its product-specific UI are separate from this backend API. Components are executable trusted code; workers are process/thread isolation, not an untrusted-code sandbox.
```js controllers/flows.js
exports.install = function() {
ROUTE('GET /flow-workers/', exercise);
};
async function exercise($) {
var ids = [];
try {
var source = '';
var out = [];
for (var worker of ['worker', true]) {
var id = worker === true ? 'file-process' : 'file-thread';
ids.push(id);
var schema = { id: id, name: id, worker: worker, components: { echo: source }, design: { a: { id: 'a', component: 'echo', config: { prefix: 'one:' }, connections: {} } }, variables: {} };
var dir = PATH.root(id);
F.Fs.mkdirSync(dir, { recursive: true });
F.Fs.writeFileSync(dir + '/demo.flow', JSON.stringify(schema));
F.Fs.writeFileSync(dir + '/ignored-bk.flow', JSON.stringify({ id: 'must-not-load' }));
await new Promise(resolve => Flow.init(dir, resolve));
var instance = Flow.instances[id];
var first = await new Promise((resolve, reject) => instance.exec({ id: 'a', data: 'hello', callback: (err, msg) => err ? reject(err) : resolve(msg.data) }));
instance.reconfigure('a', { prefix: 'two:' });
var second = await new Promise((resolve, reject) => instance.exec({ id: 'a', data: 'again', callback: (err, msg) => err ? reject(err) : resolve(msg.data) }));
out.push({ worker: worker, first: first, second: second, isolated: !!instance.flow.isworkerthread });
Flow.remove(id);
}
$.json({ results: out, removed: ids.every(id => !Flow.instances[id]), backupSkipped: !Flow.instances['must-not-load'] });
} catch (e) { $.invalid(e); }
finally { for (var id of ids) if (Flow.instances[id]) Flow.remove(id); }
}
```
## Action lifecycle hooks, result extension and administrator guards
Source: https://forge.totaljs.es/en/total5/action-hooks-and-admin/
Framework: total5@0.0.19
Content-SHA256: 96d006ef7a49b5e38bca290fca66dc60e4ee9d0bd9b5924585c9a2bbf2646d17
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`oninput($)` runs after input validation and declared middleware, immediately before the action function. `onoutput(error,response,$)` runs on the successful completion path after output filtering and optional extension. In 0.0.19 an invalid input or `$.invalid()` does not call this hook; do not treat it as a universal error/finally hook.
`extend:'result'` adds the action result to its validated input under that key and returns the extended input. A dotted path uses nested assignment. It is useful for deliberate action composition; it changes the public response contract.
Either `sa:true` or `su:true` requires a user with either administrator flag. They do not define two different privilege tiers. Normal permissions checks remain separate. Set these administrator flags only from trusted server authentication.
```js definitions/hooks.js
MAIN.hooks = [];
AUTH(function($) {
var role = $.headers['x-demo-role'];
if (role === 'sa' || role === 'su') $.success({ id:'admin', [role]:true });
else if (role === 'member') $.success({id:'member'});
else $.invalid();
});
NEWACTION('Hooks|extend', {
input: '*name:String',
extend: 'result',
oninput: function($) { MAIN.hooks.push('input:' + $.payload.name); },
onoutput: function(err, response) { MAIN.hooks.push('output:' + response.result.greeting); },
action: function($, model) { $.callback({greeting:'Hello ' + model.name}); }
});
NEWACTION('Hooks|fail', {
onoutput: function() { MAIN.hooks.push('unexpected'); },
action: function($) { $.invalid('failed'); }
});
for (var flag of ['sa','su']) {
var action = { action:function($) { $.success(); } };
action[flag] = true;
NEWACTION('Hooks|' + flag, action);
}
```
```js controllers/hooks.js
exports.install = function() {
ROUTE('POST /extend/ @json --> Hooks|extend');
ROUTE('GET /fail/ --> Hooks|fail');
ROUTE('GET /sa/ --> Hooks|sa');
ROUTE('GET /su/ --> Hooks|su');
ROUTE('GET /hook-events/', $ => $.json(MAIN.hooks));
};
```
## Real SQL with native QueryBuilder and SQLite transactions
Source: https://forge.totaljs.es/en/total5/query-builder-sqlite/
Framework: total5@0.0.19
Content-SHA256: 0d9c3083d0b11d65b113fcf1df348e839a572228f30f311ffd0b6c58ffdd5cd4
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
This example connects Total.js's native `DB()/NEWDB()` to a real file-backed SQLite database using Node's built-in `node:sqlite`. The example uses **Node 24.14.1 and total5@0.0.19**, without an npm SQL dependency. Check the [Node 24 SQLite documentation](https://nodejs.org/docs/latest-v24.x/api/sqlite.html) for runtime availability and API stability. `DatabaseSync` is synchronous: this small driver demonstrates correctness, not a recommendation for heavy blocking work on a busy HTTP thread.
This is an application-owned driver, **not the separate Total.js QueryBuilder SQL package**, nor a PostgreSQL/MySQL compatibility claim. It deliberately supports only one allowlisted table, equality filters, selected columns, sort, bounded pagination and basic CRUD. Unsupported operations fail closed. The broader builder contract is in [query-builder-contract](/en/total5/query-builder-contract/).
Values use bound `?` parameters. Table/column names and order directions are selected by server allowlists; never concatenate arbitrary identifiers from requests. Update/delete require a filter. The HTTP fixture uses fixed demonstration data; add authentication and ownership constraints before exposing real records.
A queue of `DB()` operations is not an atomic transaction. The transfer function explicitly owns one connection, begins a transaction, makes both changes synchronously, commits on success and rolls back on any error. Do not insert asynchronous waits into this single shared connection transaction. Multi-connection async drivers need connection reservation and their own transaction API.
```js definitions/sqlite.js
var DatabaseSync = require('node:sqlite').DatabaseSync;
var db = new DatabaseSync(PATH.root('ledger.sqlite'));
db.exec('CREATE TABLE accounts (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, balance INTEGER NOT NULL CHECK(balance >= 0)) STRICT');
db.prepare('INSERT INTO accounts VALUES (?, ?, ?)').run('a', 'Alice', 100);
db.prepare('INSERT INTO accounts VALUES (?, ?, ?)').run('b', 'Bob', 50);
function column(name) {
if (!['id', 'name', 'balance'].includes(name))
throw new Error('Column not allowed');
return '"' + name + '"';
}
function where(options, params) {
var filters = options.filter.map(function(f) {
if (f.type !== 'where' || f.comparer !== '=')
throw new Error('Filter not supported');
params.push(f.value);
return column(f.name) + ' = ?';
});
return filters.length ? ' WHERE ' + filters.join(' AND ') : '';
}
function integer(value, fallback, max) {
if (value == null)
return fallback;
if (!Number.isInteger(value) || value < 0 || value > max)
throw new Error('Invalid pagination');
return value;
}
NEWDB('sqlite', function(options, callback) {
try {
if (options.table !== 'accounts')
throw new Error('Table not allowed');
var params = [];
var condition = where(options, params);
var result;
if (['find','read','list','count'].includes(options.exec)) {
var count = db.prepare('SELECT COUNT(*) AS value FROM accounts' + condition).get(...params).value;
if (options.exec === 'count')
result = count;
else {
var fields = (options.fields || ['id','name','balance']).map(column).join(',');
var sort = (options.sort || ['id_asc']).map(function(s) {
var match = /^(id|name|balance)_(asc|desc)$/.exec(s);
if (!match)
throw new Error('Sort not allowed');
return column(match[1]) + ' ' + match[2];
}).join(',');
var take = integer(options.take, 100, 100);
var skip = integer(options.skip, 0, 100000);
var rows = db.prepare('SELECT ' + fields + ' FROM accounts' + condition + ' ORDER BY ' + sort + ' LIMIT ? OFFSET ?').all(...params, take, skip);
result = options.exec === 'list' ? { items: rows, count: count } : rows;
}
} else if (options.exec === 'insert') {
var p = options.payload;
db.prepare('INSERT INTO accounts (id,name,balance) VALUES (?,?,?)').run(p.id,p.name,p.balance);
result = { id: p.id };
} else if (options.exec === 'update') {
if (!condition)
throw new Error('Filter required');
var keys = Object.keys(options.payload);
if (!keys.length)
throw new Error('Empty update');
var set = keys.map(key => column(key) + ' = ?').join(',');
result = db.prepare('UPDATE accounts SET ' + set + condition).run(...keys.map(key => options.payload[key]), ...params).changes;
} else if (options.exec === 'remove') {
if (!condition)
throw new Error('Filter required');
result = db.prepare('DELETE FROM accounts' + condition).run(...params).changes;
} else
throw new Error('Operation not supported');
callback(null, result);
} catch (e) { callback(e); }
});
MAIN.transfer = function(amount, fail) {
if (!Number.isSafeInteger(amount) || amount <= 0)
throw new Error('Positive integer amount required');
db.exec('BEGIN IMMEDIATE');
try {
db.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').run(amount, 'a');
if (fail)
throw new Error('Simulated failure');
db.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').run(amount, 'b');
db.exec('COMMIT');
} catch (e) {
db.exec('ROLLBACK');
throw e;
}
};
// Reopen the actual file to verify persisted values on a second connection.
MAIN.sqlSnapshot = function() {
var reader = new DatabaseSync(PATH.root('ledger.sqlite'), { readOnly: true });
try { return reader.prepare('SELECT id,balance FROM accounts ORDER BY id').all(); }
finally { reader.close(); }
};
```
```js controllers/sqlite.js
exports.install = function() {
ROUTE('GET /sql/', exercise);
};
async function exercise($) {
try {
var out = {};
out.initial = await DB().find('sqlite/accounts').fields('id,balance').promise();
MAIN.transfer(10, false);
out.committed = MAIN.sqlSnapshot();
try { MAIN.transfer(20, true); } catch (e) { out.rollback = e.message; }
out.afterRollback = MAIN.sqlSnapshot();
try { MAIN.transfer(999, false); } catch (e) { out.constraint = e.message; }
out.afterConstraint = MAIN.sqlSnapshot();
var name = "x'); DROP TABLE accounts; --";
await DB().insert('sqlite/accounts', { id: 'c', name: name, balance: 25 }).promise();
out.literal = await DB().read('sqlite/accounts').where('name', name).promise();
out.updated = await DB().update('sqlite/accounts', { balance: 30 }).id('c').promise();
out.page = await DB().list('sqlite/accounts').sort('balance', true).page(2, 1).promise();
try { await DB().insert('sqlite/accounts', { id: 'd', name: 'Alice', balance: 1 }).promise(); } catch (e) { out.duplicate = e.message; }
try { await DB().find('sqlite/accounts').fields('password').promise(); } catch (e) { out.column = e.message; }
try { await DB().remove('sqlite/accounts').promise(); } catch (e) { out.unsafe = e.message; }
out.removed = await DB().remove('sqlite/accounts').id('c').promise();
out.missing = await DB().read('sqlite/accounts').id('c').promise();
out.count = await DB().count('sqlite/accounts').promise();
$.json(out);
} catch (e) { $.invalid(e); }
}
```
## Load application resources and avoid runtime package surprises
Source: https://forge.totaljs.es/en/total5/load-and-npminstall/
Framework: total5@0.0.19
Content-SHA256: b0349301366b0df42a87c482e3d35d66cf8cc6fa1505cc377a34aea6d934523f
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`LOAD(types, [callback], [clear=true])` is the application loader. `types` is an array or comma-separated list of resource groups (such as `config`, `definitions`, `controllers`, `actions`, `modules`, `resources`, `jsonschemas`, `flowstreams`). With no list it loads the normal application groups. It extracts bundles first and clears temporary files by default. Use it in a standalone script when you need application definitions without starting an HTTP listener; do not repeatedly call it in a request handler.
`NPMINSTALL(name, [callback])` checks the package under `CONF.$nodemodules`: an already-installed matching version returns without running npm. Otherwise it invokes `npm install` there. The name must be a trusted fixed application value, never request input. Deploy dependencies through a pinned package manifest/lockfile when reproducibility matters. The test below covers only the installed-package branch: it does not download, upgrade or certify any third-party package.
```js controllers/loading.js
exports.install = function() {
ROUTE('GET /loader-contract/', function($) { $.json({ available: typeof LOAD === 'function' && typeof NPMINSTALL === 'function' }); });
};
```
## Query a real local LDAP directory
Source: https://forge.totaljs.es/en/total5/ldap-openldap/
Framework: total5@0.0.19
Content-SHA256: acf740f174412263663ffdc967c13b624ab592c51d4cde00e0cd491fb1103357
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
`LDAP(options, callback)` is the native read client. `options.ldap` supplies `host` and `port`; `user` is the bind DN, `password` its password, `dn` the search base, and `type` the requested object class (`person` or `group`). `type:'profile'` uses `login` to search `sAMAccountName` and returns one object or `null`. `noauth:true` skips bind; it does not grant directory permissions. Always specify a search base explicitly.
`attributes` maps returned attribute names to conversion types (`string`, `number`, `date`, `buffer`, `hex`, `base64`, etc.); it is **not** an allowlist of attributes to retrieve. A directory may return scalar values or arrays when an attribute repeats. Restrict the data your HTTP API exposes.
This executable fixture requires local **OpenLDAP** tools: `slapd`, `ldapadd` and the core/cosine/inetorgperson schema files. On macOS the defaults below use the system tools; other installations can set `FORGE_SLAPD`, `FORGE_LDAPADD` and `FORGE_LDAP_SCHEMA`. It runs a separate unprivileged loopback server and temporary LDIF database, then terminates it. It never uses an existing directory. The test covers search, multiple attribute values and an invalid-bind boundary; it does not certify Active Directory compatibility, TLS/StartTLS, groups or reconnect behavior. Never send real credentials over an unprotected network.
**Decide authentication from the bind.** Check the result of the bind in your own code, and treat a returned profile as authenticated only after the bind has succeeded.
```js controllers/directory.js
exports.install = function() {
ROUTE('POST /directory-fixture/ @json', directory);
};
function directory($) {
var port = Number($.body.port);
if (!Number.isInteger(port) || port < 1024 || port > 65535)
return $.invalid(400);
LDAP({
ldap: { host: '127.0.0.1', port: port },
user: 'cn=admin,dc=forge,dc=test',
password: $.body.wrong ? 'incorrect' : 'local-fixture-only',
dn: 'dc=forge,dc=test',
type: 'person',
attributes: { cn: 'string', mail: 'string' }
}, function(err, people) {
if (err)
$.json({ failed: true });
else
$.json({ people: people.map(person => ({ cn: person.cn, mail: person.mail })) });
});
}
```
## Package application files as a Total.js bundle
Source: https://forge.totaljs.es/en/total5/application-bundles/
Framework: total5@0.0.19
Content-SHA256: 0d5c2183cc37e702073e256ef69943b26b22add5405e25c67915140353b0a76c
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
A Total.js application bundle is an archive created with `BACKUP()`, placed in the application's `bundles/` directory with the `.bundle` suffix. At startup `LOAD()` extracts bundles and combines application files into `.src/`; the framework then uses that directory as its application root. Bundle filenames containing `-bk.bundle` or `_bk.bundle` are ignored. A `bundles.debug` file reuses the already extracted `.src` directory.
The source also supports `.url` files that fetch remote bundle archives. Bundle files contain executable application code: load trusted archives only. Keep credentials, runtime databases and private files out of distributable archives. `BACKUP` is packaging, not encryption.
```js controllers/bundles.js
exports.install = function() {
ROUTE('GET /bundle-loader/', $ => $.json({ load: typeof LOAD, backup: typeof BACKUP }));
};
```
## Own uploaded-file cleanup and build query links
Source: https://forge.totaljs.es/en/total5/upload-lifecycle-and-href/
Framework: total5@0.0.19
Content-SHA256: 5d7d9072b52f3dcc841f23b413c213e6a47f83eae4a521c8252f4f81d7f3f3c9
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded
Uploaded temporary files normally disappear when the request is released. `$.autoclear(false)` keeps them for work that outlives the response: your application must then move or remove them. `$.clear()` schedules removal of removable uploads and empties `$.files`; disk deletion is asynchronous. Never store arbitrary paths supplied by a client for later deletion.
In 0.0.19 build query links with the public `QUERIFY()` helper and a copy of the query, as this recipe shows. It produces a query string, not a redirect or an absolute URL.
The retained-file map and cleanup endpoints below are a local fixture; an application should use scoped ownership and durable storage rather than expose these endpoints.
```js controllers/lifecycle.js
var paths = Object.create(null);
exports.install = function() {
ROUTE('POST /upload/{mode}/ @upload <1mb', function($) {
var file = $.files[0];
if (!file) return $.invalid(400);
var id = UID();
paths[id] = file.path;
if ($.params.mode === 'retain') $.autoclear(false);
if ($.params.mode === 'clear') $.clear();
$.json({ id: id, files: $.files.length });
});
ROUTE('GET /exists/{id}/', $ => $.json({ exists: !!paths[$.params.id] && F.Fs.existsSync(paths[$.params.id]) }));
ROUTE('POST /cleanup/{id}/', function($) {
var path = paths[$.params.id];
if (path) { F.Fs.rmSync(path, { force: true }); delete paths[$.params.id]; }
$.success();
});
ROUTE('GET /link/', function($) {
var broken = false;
try { $.href('page', '2'); } catch (e) { broken = e instanceof TypeError; }
$.json({ broken: broken, link: QUERIFY('', Object.assign({}, $.query, { page: '2 & more' })) });
});
};
```
## Get started with jComponent 20
Source: https://forge.totaljs.es/en/jcomponent/getting-started/
Framework: jcomponent@20
Content-SHA256: d000a133f8599ffc3236793b3d02f382adbbf017f136adfafbc88bff4f39c05d
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
jComponent 20 is a single file (`spa.min@20.js`) that already contains jQuery. It keeps a global **state** object: you write to it with `SET(path, value)`, read from it with `GET(path)`, and elements react to changes. The bundle also has a stylesheet, `spa.min@20.css`, with the Total.js icon font. Load both from the CDN, as every example of this site does: `` and ``. If you only want the icons, load just `//cdn.componentator.com/icons.min.css` (no script is needed). The checks of this site answer the requests for `spa.min@20.js` and `spa.min@20.css` with a local copy of the same version and do not load the icons file.
`` binds an element to a path in the state. The `config` attribute lists commands separated by semicolons; `text:value` sets the text of the element to the value of the path.
```html public/index.html
jComponent
Hello
```
Call `SET('user.name', 'Louis')` again and the heading changes without any DOM code of yours.
## Read, change and watch the state
Source: https://forge.totaljs.es/en/jcomponent/state-api/
Framework: jcomponent@20
Content-SHA256: 2211f1567f71e3f701549215ecc3a1e32c676105199a4d8d5a61a52708a78c4c
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
The state is one global object addressed by **paths** such as `cart.items`. The functions to work with it:
- `SET(path, value)` writes a value and notifies everything bound to the path
- `GET(path)` reads a value
- `UPD(path)` notifies the path after you changed the object in place (`GET('cart').discount = 5; UPD('cart')`)
- `PUSH(path, value)` appends to an array and notifies; with a `^` before the path it adds the value to the beginning
- `WATCH(path, callback)` runs the callback with the new value every time the path changes; join several paths with `+`
```html public/index.html
jComponent
```
## Write a custom jComponent
Source: https://forge.totaljs.es/en/jcomponent/custom-component/
Framework: jcomponent@20
Content-SHA256: e7cce28ba59dc0aeba03d3e4d38b42f387d4ab3717e831f55440d79e4914233b
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
A component is declared with `COMPONENT(name, config, declaration)`. The second argument is the default configuration as a string (`key:value;key:value`). The declaration receives `self` (the component instance) and `config` (the parsed configuration).
- `self.make` runs once, when the component is created. Build the DOM here.
- `self.setter(value)` runs every time the value of the component's `path` changes.
- `self.set(value)` writes a new value to the `path`; `self.get()` reads it.
Use the component with a `` element. `name` selects the declaration, `path` the state and `config` overrides the defaults.
```html public/index.html
jComponent
Total:
```
The `ui-bind` next to the component shows that both read the same state: clicking the button changes the path and every element bound to it updates.
## Configure a component and reconfigure it later
Source: https://forge.totaljs.es/en/jcomponent/component-config/
Framework: jcomponent@20
Content-SHA256: 0029f1b2c2a136827eb199e9ed2afef07a7637807acf316edd4f61f7011cad76
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
The second argument of `COMPONENT()` holds the default options. The `config` attribute of the element overrides them, and the declaration reads the merged values from `config`. Implement `self.configure(key, value)` to react when an option is set or changed. At runtime `self.reconfigure('key:value;key2:value2')` (or `CONFIG(selector, config)` for many components) changes options and calls `configure` for each changed key.
```html public/index.html
jComponent
```
## Component lifecycle: make, setter and destroy
Source: https://forge.totaljs.es/en/jcomponent/component-lifecycle/
Framework: jcomponent@20
Content-SHA256: a91ae5a1c166759e760b643f70e1f3cfe3cd06a062d201b231e646ef7081f169
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
A component goes through three moments you can hook into:
- `self.make()` runs once, when the component is created and its element is ready: build the DOM and register events here
- `self.setter(value, path, type)` runs every time the value of the `path` changes (also the first time)
- `self.destroy()` runs after the element was removed from the page: stop timers and release what you created. jComponent detects removed elements with a cleanup that runs every minute; call `FREE()` to run it immediately
`self.readonly()` tells jComponent the component never writes to its path, which turns off its validation state.
```html public/index.html
jComponent
```
## Validate input with a component's validate()
Source: https://forge.totaljs.es/en/jcomponent/validation/
Framework: jcomponent@20
Content-SHA256: da083e2d978ec1a4472abb4b2d8a9ae7b2b3d28b560d6e54ea3a9114ed7be5f7
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
A component that edits a value can validate it. Define `self.validate(value)` and return `true` (or an empty string) when the value is valid, or `false` (or a message) when it is not. Write the value back with `self.modify(value)`: it also marks the component as **modified** and **touched**.
jComponent keeps these states in `config` (`invalid`, `touched`, `modified`) and as CSS classes (`ui-invalid`, `ui-touched`, `ui-modified`), so styling needs no code. `ERRORS(path, callback)` returns the invalid components below a path, which is what a form needs before it submits, and `RESET(path)` clears the touched and modified states.
```html public/index.html
jComponent
```
## Path prefixes: common, code lists, temporary state and plugin paths
Source: https://forge.totaljs.es/en/jcomponent/path-prefixes/
Framework: jcomponent@20
Content-SHA256: bb875905d62b28b998f81784d6ebbe4244d28c88f46adfb3bfbf9b45754c2c9b
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
A path is where a value lives in the state of the page, and in jComponent 20 its **first character** can change the place.
| Prefix | Example | Where it points |
|---|---|---|
| `*` | `*user.name` | the common state: `common.user.name` (`*` alone is `common`) |
| `#` | `#countries` | the code lists: `DEF.cl.countries`, where `CL()` stores the lists it loads |
| `%` | `%step` | the temporary state: `DEF.tmp.step`, for values that do not belong to the model |
| `?` | `?.items` | inside a ``: the path of that plugin (see the plugin recipe) |
| `plugin\|path` | `cart\|items` | a property of the plugin object called `cart`, from anywhere: `PLUGINS["cart"].items` (it is **not** the state path `cart.items`) |
The flags of a path (`@visible`, `@change`...) are a different thing: they come after a space (see the custom flags recipe). Paths without a prefix are plain paths of the state (`user.name`).
```html public/index.html
jComponent
```
## Load a library before a component starts
Source: https://forge.totaljs.es/en/jcomponent/component-dependencies/
Framework: jcomponent@20
Content-SHA256: 1343e12a6dec77d0fadee875a09f8a990ec5f2291032789c23e3d9ad97689498
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
The fourth argument of `COMPONENT()` is a list of **dependencies**: URLs of scripts (`.js`) or styles (`.css`) that the component needs. jComponent downloads each one only once, in order, and creates the component after they are loaded, so `make()` can rely on the library being there. This is the way to wrap a third-party library into a component without loading it on pages that do not use it.
```js public/js/starslib.js
window.starslib = {
render: function(count) {
var html = '';
for (var i = 0; i < count; i++)
html += '*';
return html;
}
};
```
```html public/index.html
jComponent
```
## Extend components and plugins with EXTENSION
Source: https://forge.totaljs.es/en/jcomponent/extensions/
Framework: jcomponent@20
Content-SHA256: 36d8b89a461439666c01affaa521ef83de6b58de563a0bdc49c5d9cc976298f2
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
`EXTENSION(name, config, callback)` adds code to every instance of the component `name` (or of the plugin `@name`) when it is created. The callback receives the instance and its config, exactly like a declaration, so you can add methods, change options or listen to events. It is how an application customizes third-party components without forking them.
```html public/index.html
jComponent
```
## Show, hide and style elements with ui-bind
Source: https://forge.totaljs.es/en/jcomponent/ui-bind/
Framework: jcomponent@20
Content-SHA256: 482f5f41510459159e22387a9b057687f79a009fb31c96783b3cff31a0a33fd7
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
`` applies **commands** to its element whenever the value of the path changes. Commands are separated by semicolons. Each command is `name:expression`, where `value` is the current value of the path:
- `show:value` / `hide:value` shows or hides the element
- `html:value` / `text:value` replaces the content
- `.classname:value` adds the class when the expression is truthy
```html public/index.html
jComponent
Admin panel
You are a guest
-
```
## Render lists with a ui-bind template
Source: https://forge.totaljs.es/en/jcomponent/ui-bind-templates/
Framework: jcomponent@20
Content-SHA256: 7e72862392e0c19c808e973b30a82d3c1551828fea364d0b3e1bb0f65556d668
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: jcomponent20@3fedd009a277f50fddc608b97afbec553b67c51b
Review: not-recorded
With the `template` command, `` renders a **Tangular** template instead of setting one value. The template is the `