nerak repo
This commit is contained in:
@@ -1243,7 +1243,7 @@ task("daily_digest", {
|
|||||||
**`.errors` / `.repairs`** *(per task)*: each task can declare its own handlers, resolved the same way as resource pipelines (the task's own handlers, then its module's). See [Error and Repair Pipelines](#error-and-repair-pipelines).
|
**`.errors` / `.repairs`** *(per task)*: each task can declare its own handlers, resolved the same way as resource pipelines (the task's own handlers, then its module's). See [Error and Repair Pipelines](#error-and-repair-pipelines).
|
||||||
```c
|
```c
|
||||||
task("send_invoice", {
|
task("send_invoice", {
|
||||||
fetch({.url = "https://api.billing.dev/invoices/{{invoice_id}}", .ctx_key = "inv"})
|
fetch({n_get, "https://api.billing.dev/invoices/{{invoice_id}}", "inv"})
|
||||||
}, .repairs = {{n_not_authorized, {run(.call = refresh_billing_token)}}});
|
}, .repairs = {{n_not_authorized, {run(.call = refresh_billing_token)}}});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1289,43 +1289,43 @@ Built-in regex macros are defined in `nerak.h`; define your own the same way: `#
|
|||||||
|
|
||||||
**`.ctx_key` *(by order)***: name of the parameter to validate.
|
**`.ctx_key` *(by order)***: name of the parameter to validate.
|
||||||
```c
|
```c
|
||||||
input({"title", "^\\S+$", "required"})
|
input({.ctx_key = "title", .regex = "^\\S+$", .err_msg = "required"})
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.regex` *(by order)***: regex pattern, or a built-in validator macro.
|
**`.regex` *(by order)***: regex pattern, or a built-in validator macro.
|
||||||
```c
|
```c
|
||||||
input({"email", n_email, "bad email"})
|
input({.ctx_key = "email", .regex = n_email, .err_msg = "bad email"})
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.err_msg` *(by order)***: human-readable error shown via `{{error_message:name}}`.
|
**`.err_msg` *(by order)***: human-readable error shown via `{{error_message:name}}`.
|
||||||
```c
|
```c
|
||||||
input({"age", n_int, "must be a number"})
|
input({.ctx_key = "age", .regex = n_int, .err_msg = "must be a number"})
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.opt`**: skip validation when the parameter is absent.
|
**`.opt`**: skip validation when the parameter is absent.
|
||||||
```c
|
```c
|
||||||
input({"filter", "^(active|done)$", .opt = true})
|
input({.ctx_key = "filter", .regex = "^(active|done)$", .opt = true})
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.def`**: default value injected when the parameter is absent.
|
**`.def`**: default value injected when the parameter is absent.
|
||||||
```c
|
```c
|
||||||
input({"page", n_int, .def = "1"})
|
input({.ctx_key = "page", .regex = n_int, .def = "1"})
|
||||||
```
|
```
|
||||||
|
|
||||||
Combined:
|
Combined:
|
||||||
```c
|
```c
|
||||||
input(
|
input(
|
||||||
{"email", n_email, "must be a valid email"},
|
{.ctx_key = "email", .regex = n_email, .err_msg = "must be a valid email"},
|
||||||
{"title", n_not_empty, "cannot be empty"},
|
{.ctx_key = "title", .regex = n_not_empty, .err_msg = "cannot be empty"},
|
||||||
{"page", n_int, "must be a number", .def = "1"},
|
{.ctx_key = "page", .regex = n_int, .err_msg = "must be a number", .def = "1"},
|
||||||
{"filter", "^(active|done)$", "must be 'active' or 'done'", .opt = true},
|
{.ctx_key = "filter", .regex = "^(active|done)$", .err_msg = "must be 'active' or 'done'", .opt = true},
|
||||||
{"username", n_user, "must be alphanumeric"}
|
{.ctx_key = "username", .regex = n_user, .err_msg = "must be alphanumeric"}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
For checks beyond regex (uniqueness, cross-field rules, lookups), pair `input()` with a query and `run()`:
|
For checks beyond regex (uniqueness, cross-field rules, lookups), pair `input()` with a query and `run()`:
|
||||||
```c
|
```c
|
||||||
input({"username", n_user, "must be alphanumeric"}),
|
input({.ctx_key = "username", .regex = n_user, .err_msg = "must be alphanumeric"}),
|
||||||
sqlite_query({"users_db", "find_username", "existing"}),
|
sqlite_query({"users_db", "find_username", "existing"}),
|
||||||
run(^(){
|
run(^(){
|
||||||
auto rows = get("existing");
|
auto rows = get("existing");
|
||||||
@@ -1350,39 +1350,39 @@ Each engine provides its own query step: `sqlite_query()`, `postgres_query()`, `
|
|||||||
|
|
||||||
**`.db` *(by order)***: database name, matching the name a `<engine>_config(...)` was registered with.
|
**`.db` *(by order)***: database name, matching the name a `<engine>_config(...)` was registered with.
|
||||||
```c
|
```c
|
||||||
sqlite_query({"todos_db", "get_todos", "todos_data"})
|
sqlite_query({.db = "todos_db", .q = "get_todos", .ctx_key = "todos_data"})
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.q` *(by order)***: context key holding the SQL to run.
|
**`.q` *(by order)***: context key holding the SQL to run.
|
||||||
```c
|
```c
|
||||||
sqlite_query({"todos_db", "get_todos", "todos_data"})
|
sqlite_query({.db = "todos_db", .q = "get_todos", .ctx_key = "todos_data"})
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.ctx_key` *(by order)***: context key for the result table. Optional; omit when the result isn't needed (e.g. an insert without `RETURNING`).
|
**`.ctx_key` *(by order)***: context key for the result table. Optional; omit when the result isn't needed (e.g. an insert without `RETURNING`).
|
||||||
```c
|
```c
|
||||||
sqlite_query({"todos_db", "create_todo"}) // no result captured
|
sqlite_query({.db = "todos_db", .q = "create_todo"}) // no result captured
|
||||||
sqlite_query({"todos_db", "get_todos", "todos_data"}) // result under "todos_data"
|
sqlite_query({.db = "todos_db", .q = "get_todos", .ctx_key = "todos_data"}) // result under "todos_data"
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.err_on_empty`**: when true, raise `404 Not Found` if the query affects/returns zero rows. Default false.
|
**`.err_on_empty`**: when true, raise `404 Not Found` if the query affects/returns zero rows. Default false.
|
||||||
```c
|
```c
|
||||||
sqlite_query({"todos_db", "get_todo", "todo", .err_on_empty = true})
|
sqlite_query({.db = "todos_db", .q = "get_todo", .ctx_key = "todo", .err_on_empty = true})
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.if_ctx` / `.not_ctx`** *(per item)*: conditionally include or skip individual queries while running the others concurrently.
|
**`.if_ctx` / `.not_ctx`** *(per item)*: conditionally include or skip individual queries while running the others concurrently.
|
||||||
```c
|
```c
|
||||||
sqlite_query(
|
sqlite_query(
|
||||||
{"db", "get_todos", "todos_data"},
|
{.db = "db", .q = "get_todos", .ctx_key = "todos_data"},
|
||||||
{"db", "get_urgent", "urgent", .if_ctx = "show_urgent"}
|
{.db = "db", .q = "get_urgent", .ctx_key = "urgent", .if_ctx = "show_urgent"}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Combined:
|
Combined:
|
||||||
```c
|
```c
|
||||||
sqlite_query(
|
sqlite_query(
|
||||||
{"todos_db", "get_todos", "todos_data"},
|
{.db = "todos_db", .q = "get_todos", .ctx_key = "todos_data"},
|
||||||
{"todos_db", "get_todo", "todo", .err_on_empty = true},
|
{.db = "todos_db", .q = "get_todo", .ctx_key = "todo", .err_on_empty = true},
|
||||||
{"todos_db", "get_urgent", "urgent", .if_ctx = "show_urgent"}
|
{.db = "todos_db", .q = "get_urgent", .ctx_key = "urgent", .if_ctx = "show_urgent"}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1417,7 +1417,7 @@ Nests records from one context table into each matching record of another, like
|
|||||||
|
|
||||||
Combined:
|
Combined:
|
||||||
```c
|
```c
|
||||||
join("projects", "id", "todos", "project_id")
|
join(.parent_ctx_key = "projects", .parent_field_key = "id", .child_ctx_key = "todos", .child_field_key = "project_id")
|
||||||
```
|
```
|
||||||
|
|
||||||
**Full context example.** Concurrent query → `join()` → `mustache()`: fetch parent and children from separate queries, render as one nested structure. Blog + comments, single database:
|
**Full context example.** Concurrent query → `join()` → `mustache()`: fetch parent and children from separate queries, render as one nested structure. Blog + comments, single database:
|
||||||
@@ -1450,7 +1450,7 @@ resource("blog", "/blogs/:id",
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Nest each comment into its matching blog record
|
// Nest each comment into its matching blog record
|
||||||
join("blog", "id", "comments", "blog_id"),
|
join(.parent_ctx_key = "blog", .parent_field_key = "id", .child_ctx_key = "comments", .child_field_key = "blog_id"),
|
||||||
|
|
||||||
// Enter {{#blog}} first; after join(), comments lives INSIDE each blog record
|
// Enter {{#blog}} first; after join(), comments lives INSIDE each blog record
|
||||||
mustache("blog", "blog_s"),
|
mustache("blog", "blog_s"),
|
||||||
@@ -1606,7 +1606,7 @@ Pushes a Server-Sent Event. With `.chan`, the event broadcasts to all clients on
|
|||||||
|
|
||||||
**`.chan` *(by order)***: channel to broadcast on; supports `{{interpolation}}`.
|
**`.chan` *(by order)***: channel to broadcast on; supports `{{interpolation}}`.
|
||||||
```c
|
```c
|
||||||
sse("todos:{{user_id}}", .event = "new_todo", .data = {"{{todo}}"})
|
sse(.chan = "todos:{{user_id}}", .event = "new_todo", .data = {"{{todo}}"})
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.event`**: SSE `event:` line value.
|
**`.event`**: SSE `event:` line value.
|
||||||
@@ -1626,7 +1626,7 @@ sse(.comment = "keep-alive")
|
|||||||
|
|
||||||
Combined:
|
Combined:
|
||||||
```c
|
```c
|
||||||
sse("todos:{{user_id}}",
|
sse(.chan = "todos:{{user_id}}",
|
||||||
.event = "todo_updated",
|
.event = "todo_updated",
|
||||||
.data = {"id: {{todo_id}}", "title: {{title}}"},
|
.data = {"id: {{todo_id}}", "title: {{title}}"},
|
||||||
.comment = "broadcast at {{timestamp}}"
|
.comment = "broadcast at {{timestamp}}"
|
||||||
@@ -1639,23 +1639,23 @@ Renders a template into the pipeline context. `mustache()` renders Mustache; `md
|
|||||||
|
|
||||||
**`.template_ctx_key` *(by order)***: context key holding the template string to render.
|
**`.template_ctx_key` *(by order)***: context key holding the template string to render.
|
||||||
```c
|
```c
|
||||||
mustache("todos", "todos_s")
|
mustache(.template_ctx_key = "todos", .ctx_key = "todos_s")
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.ctx_key` *(by order)***: context key to write the rendered output to.
|
**`.ctx_key` *(by order)***: context key to write the rendered output to.
|
||||||
```c
|
```c
|
||||||
mustache("todos", "todos_s")
|
mustache(.template_ctx_key = "todos", .ctx_key = "todos_s")
|
||||||
```
|
```
|
||||||
|
|
||||||
JSON:
|
JSON:
|
||||||
```c
|
```c
|
||||||
json("todos", "todos_j")
|
json(.template_ctx_key = "todos", .ctx_key = "todos_j")
|
||||||
```
|
```
|
||||||
|
|
||||||
Markdown-with-Mustache:
|
Markdown-with-Mustache:
|
||||||
```c
|
```c
|
||||||
context("welcome", "# Welcome, {{user_name}}");
|
context("welcome", "# Welcome, {{user_name}}");
|
||||||
mdm("welcome", "welcome_s")
|
mdm(.template_ctx_key = "welcome", .ctx_key = "welcome_s")
|
||||||
```
|
```
|
||||||
|
|
||||||
#### respond
|
#### respond
|
||||||
@@ -1664,23 +1664,23 @@ Sends a pipeline context value as the HTTP response.
|
|||||||
|
|
||||||
**`.ctx_key` *(by order)***: key of the rendered content to send.
|
**`.ctx_key` *(by order)***: key of the rendered content to send.
|
||||||
```c
|
```c
|
||||||
respond("todos_s")
|
respond(.ctx_key = "todos_s")
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.status`**: HTTP response status (default `n_ok`). Values: `n_ok` (200), `n_created` (201), `n_redirect` (302), `n_bad_request` (400), `n_not_authorized` (401), `n_not_found` (404), `n_error` (500).
|
**`.status`**: HTTP response status (default `n_ok`). Values: `n_ok` (200), `n_created` (201), `n_redirect` (302), `n_bad_request` (400), `n_not_authorized` (401), `n_not_found` (404), `n_error` (500).
|
||||||
```c
|
```c
|
||||||
respond("not_found_s", .status = n_not_found)
|
respond(.ctx_key = "not_found_s", .status = n_not_found)
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.mime`**: override the response content type. Values: `n_html`, `n_txt`, `n_es`, `n_json`, `n_js`.
|
**`.mime`**: override the response content type. Values: `n_html`, `n_txt`, `n_es`, `n_json`, `n_js`.
|
||||||
```c
|
```c
|
||||||
respond("plain_s", .mime = n_txt)
|
respond(.ctx_key = "plain_s", .mime = n_txt)
|
||||||
```
|
```
|
||||||
|
|
||||||
Combined:
|
Combined:
|
||||||
```c
|
```c
|
||||||
mustache("not_found", "not_found_s"),
|
mustache("not_found", "not_found_s"),
|
||||||
respond("not_found_s", .status = n_not_found)
|
respond(.ctx_key = "not_found_s", .status = n_not_found)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### headers and cookies
|
#### headers and cookies
|
||||||
@@ -1961,7 +1961,7 @@ respond("standard_s", .not_ctx = "is_urgent")
|
|||||||
```c
|
```c
|
||||||
// One request per row in `users`, all concurrent.
|
// One request per row in `users`, all concurrent.
|
||||||
// Each row's `id` fills the URL; responses collected into `profiles`, aligned with `users`.
|
// Each row's `id` fills the URL; responses collected into `profiles`, aligned with `users`.
|
||||||
fetch({.url = "https://api.users.dev/{{id}}", .ctx_key = "profiles", .map = "users"})
|
fetch({n_get, "https://api.users.dev/{{id}}", "profiles", .map = "users"})
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.map_key`**: context key under which the current row is exposed as a single-row table. Pairs with `.map`.
|
**`.map_key`**: context key under which the current row is exposed as a single-row table. Pairs with `.map`.
|
||||||
@@ -2037,9 +2037,9 @@ For `GET /todos/5` the executed order is: `input({"id", ...})` (resource `.all`)
|
|||||||
|
|
||||||
config(blogs){
|
config(blogs){
|
||||||
sqlite_config(
|
sqlite_config(
|
||||||
.name = "blog_db",
|
"blog_db",
|
||||||
.conn = "file:blogs.db?mode=rwc",
|
"file:blogs.db?mode=rwc",
|
||||||
.migrations = {"create_blogs_table", "create_comments_table"}
|
{"create_blogs_table", "create_comments_table"}
|
||||||
);
|
);
|
||||||
|
|
||||||
resource("blog", "/blogs/:id",
|
resource("blog", "/blogs/:id",
|
||||||
@@ -2119,7 +2119,7 @@ Activate with `#include <datastar.h>`. Serves the Datastar runtime as the `{{> d
|
|||||||
**`.chan` *(by order)***: channel to push to.
|
**`.chan` *(by order)***: channel to push to.
|
||||||
```c
|
```c
|
||||||
mustache("todo_row", "todo_row_s"),
|
mustache("todo_row", "todo_row_s"),
|
||||||
datastar("todos:{{user_id}}", .target = "#todo-list", .mode = ds_append, .elements = "todo_row_s")
|
datastar(.chan = "todos:{{user_id}}", .target = "#todo-list", .mode = ds_append, .elements = "todo_row_s")
|
||||||
```
|
```
|
||||||
|
|
||||||
**`.target`**: element id or CSS selector for the element to patch; supports `{{interpolation}}`.
|
**`.target`**: element id or CSS selector for the element to patch; supports `{{interpolation}}`.
|
||||||
@@ -2162,7 +2162,7 @@ resource("todos", "/todos",
|
|||||||
sqlite_query({"todos_db", "insert_todo", "todo", .err_on_empty = true}),
|
sqlite_query({"todos_db", "insert_todo", "todo", .err_on_empty = true}),
|
||||||
// Render the new row, then patch it into the list for everyone on the channel.
|
// Render the new row, then patch it into the list for everyone on the channel.
|
||||||
mustache("todo_row", "todo_row_s"),
|
mustache("todo_row", "todo_row_s"),
|
||||||
datastar("todos:{{user_id}}",
|
datastar(.chan = "todos:{{user_id}}",
|
||||||
.target = "#todo-list",
|
.target = "#todo-list",
|
||||||
.mode = ds_append,
|
.mode = ds_append,
|
||||||
.elements = "todo_row_s"
|
.elements = "todo_row_s"
|
||||||
@@ -2173,7 +2173,7 @@ resource("todos", "/todos",
|
|||||||
|
|
||||||
Removing an element needs only a selector:
|
Removing an element needs only a selector:
|
||||||
```c
|
```c
|
||||||
datastar("todos:{{user_id}}", .target = "#todo-{{id}}", .mode = ds_remove)
|
datastar(.chan = "todos:{{user_id}}", .target = "#todo-{{id}}", .mode = ds_remove)
|
||||||
```
|
```
|
||||||
|
|
||||||
Datastar sets a context flag on requests it originates, usable with `.if_ctx`.
|
Datastar sets a context flag on requests it originates, usable with `.if_ctx`.
|
||||||
|
|||||||
+13
-13
@@ -101,10 +101,10 @@ On success promotes `input:name` → app scope. On failure writes `error:name` a
|
|||||||
By order: `{ctx_key, regex, err_msg}`. Named: `.opt` (skip if absent), `.def` (default when absent). `regex` is a regex string or a built-in macro. Define custom macros: `#define n_zipcode "^\\d{5}$"`.
|
By order: `{ctx_key, regex, err_msg}`. Named: `.opt` (skip if absent), `.def` (default when absent). `regex` is a regex string or a built-in macro. Define custom macros: `#define n_zipcode "^\\d{5}$"`.
|
||||||
```c
|
```c
|
||||||
input(
|
input(
|
||||||
{"email", n_email, "must be a valid email"},
|
{.ctx_key = "email", .regex = n_email, .err_msg = "must be a valid email"},
|
||||||
{"title", n_not_empty, "cannot be empty"},
|
{.ctx_key = "title", .regex = n_not_empty, .err_msg = "cannot be empty"},
|
||||||
{"page", n_int, "must be a number", .def = "1"},
|
{.ctx_key = "page", .regex = n_int, .err_msg = "must be a number", .def = "1"},
|
||||||
{"filter", "^(active|done)$", "must be 'active' or 'done'", .opt = true}
|
{.ctx_key = "filter", .regex = "^(active|done)$", .err_msg = "must be 'active' or 'done'", .opt = true}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
Built-in validators: `n_not_empty n_alpha n_alphanum n_slug n_no_html` · `n_int n_positive n_float n_percent` · `n_email n_uuid n_user` · `n_date n_time n_datetime` · `n_url n_ipv4 n_hex_color` · `n_zip n_phone n_cron` · `n_token n_base64` · `n_bool n_yes_no n_on_off`.
|
Built-in validators: `n_not_empty n_alpha n_alphanum n_slug n_no_html` · `n_int n_positive n_float n_percent` · `n_email n_uuid n_user` · `n_date n_time n_datetime` · `n_url n_ipv4 n_hex_color` · `n_zip n_phone n_cron` · `n_token n_base64` · `n_bool n_yes_no n_on_off`.
|
||||||
@@ -115,11 +115,11 @@ For non-regex checks (uniqueness, cross-field), pair with a query + `run()` call
|
|||||||
By order per item: `{db, q, ctx_key}`. `ctx_key` optional (omit for inserts without `RETURNING`). Named: `.err_on_empty = true` (404 if zero rows), `.if_ctx`/`.not_ctx` (per item).
|
By order per item: `{db, q, ctx_key}`. `ctx_key` optional (omit for inserts without `RETURNING`). Named: `.err_on_empty = true` (404 if zero rows), `.if_ctx`/`.not_ctx` (per item).
|
||||||
```c
|
```c
|
||||||
sqlite_query(
|
sqlite_query(
|
||||||
{"todos_db", "get_todos", "todos_data"},
|
{.db = "todos_db", .q = "get_todos", .ctx_key = "todos_data"},
|
||||||
{"todos_db", "get_todo", "todo", .err_on_empty = true},
|
{.db = "todos_db", .q = "get_todo", .ctx_key = "todo", .err_on_empty = true},
|
||||||
{"todos_db", "get_urgent","urgent", .if_ctx = "show_urgent"}
|
{.db = "todos_db", .q = "get_urgent", .ctx_key = "urgent", .if_ctx = "show_urgent"}
|
||||||
)
|
)
|
||||||
sqlite_query({"todos_db", "create_todo"}) // no result captured
|
sqlite_query({.db = "todos_db", .q = "create_todo"}) // no result captured
|
||||||
```
|
```
|
||||||
SQL file uses bound params: `select id, title from todos where id = {{id}};` and `insert into todos(title) values({{title}});`.
|
SQL file uses bound params: `select id, title from todos where id = {{id}};` and `insert into todos(title) values({{title}});`.
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ SQL file uses bound params: `select id, title from todos where id = {{id}};` and
|
|||||||
By order: `join(parent_ctx_key, parent_field_key, child_ctx_key, child_field_key)`. Optional `.parent_join_key` (new field name on parent records; defaults to the child table name). After it, each parent record gains a field holding its matched child records.
|
By order: `join(parent_ctx_key, parent_field_key, child_ctx_key, child_field_key)`. Optional `.parent_join_key` (new field name on parent records; defaults to the child table name). After it, each parent record gains a field holding its matched child records.
|
||||||
```c
|
```c
|
||||||
// before: { blog:[{id,...}], comments:[{id,blog_id,...}] }
|
// before: { blog:[{id,...}], comments:[{id,blog_id,...}] }
|
||||||
join("blog", "id", "comments", "blog_id")
|
join(.parent_ctx_key = "blog", .parent_field_key = "id", .child_ctx_key = "comments", .child_field_key = "blog_id")
|
||||||
// after: { blog:[{id,..., comments:[{...}]}] } -> template: {{#blog}}...{{#comments}}{{body}}{{/comments}}{{/blog}}
|
// after: { blog:[{id,..., comments:[{...}]}] } -> template: {{#blog}}...{{#comments}}{{body}}{{/comments}}{{/blog}}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ run(.call = assign_opponents)
|
|||||||
### sse — push a Server-Sent Event
|
### sse — push a Server-Sent Event
|
||||||
With `.chan` (by order, first value, supports interpolation) broadcasts to all clients on the channel; without it, returns to the requester. Named: `.event` (event line), `.data` (array of strings, one per data line), `.comment` (comment/keep-alive line).
|
With `.chan` (by order, first value, supports interpolation) broadcasts to all clients on the channel; without it, returns to the requester. Named: `.event` (event line), `.data` (array of strings, one per data line), `.comment` (comment/keep-alive line).
|
||||||
```c
|
```c
|
||||||
sse("todos:{{user_id}}", .event = "todo_updated", .data = {"id: {{todo_id}}", "title: {{title}}"})
|
sse(.chan = "todos:{{user_id}}", .event = "todo_updated", .data = {"id: {{todo_id}}", "title: {{title}}"})
|
||||||
```
|
```
|
||||||
|
|
||||||
### render — mustache / mdm / json
|
### render — mustache / mdm / json
|
||||||
@@ -176,7 +176,7 @@ sse("todos:{{user_id}}", .event = "todo_updated", .data = {"id: {{todo_id}}", "t
|
|||||||
### respond — send the response
|
### respond — send the response
|
||||||
By order: `respond(ctx_key)`. Named: `.status` (default `n_ok`; values `n_ok` 200, `n_created` 201, `n_redirect` 302, `n_bad_request` 400, `n_not_authorized` 401, `n_not_found` 404, `n_error` 500), `.mime` (override; same values as resource `.mime`).
|
By order: `respond(ctx_key)`. Named: `.status` (default `n_ok`; values `n_ok` 200, `n_created` 201, `n_redirect` 302, `n_bad_request` 400, `n_not_authorized` 401, `n_not_found` 404, `n_error` 500), `.mime` (override; same values as resource `.mime`).
|
||||||
```c
|
```c
|
||||||
mustache("404","not_found_s"), respond("not_found_s", .status = n_not_found)
|
mustache("404","not_found_s"), respond(.ctx_key = "not_found_s", .status = n_not_found)
|
||||||
```
|
```
|
||||||
|
|
||||||
### headers / cookies — set response headers/cookies
|
### headers / cookies — set response headers/cookies
|
||||||
@@ -224,7 +224,7 @@ respond("page_s", .not_ctx = "is_htmx")
|
|||||||
|
|
||||||
`.map = "table"` runs a step once per row, ALL ROWS CONCURRENTLY; the row's fields land in scope as bare `{{interpolations}}`. `.map_key = "key"` (pairs with `.map`) exposes the current row as a single-row table under `key` (useful for `mustache`). With a `ctx_key`, results collect into a table aligned with the input (one entry per row).
|
`.map = "table"` runs a step once per row, ALL ROWS CONCURRENTLY; the row's fields land in scope as bare `{{interpolations}}`. `.map_key = "key"` (pairs with `.map`) exposes the current row as a single-row table under `key` (useful for `mustache`). With a `ctx_key`, results collect into a table aligned with the input (one entry per row).
|
||||||
```c
|
```c
|
||||||
fetch({.url = "https://api.users.dev/{{id}}", .ctx_key = "profiles", .map = "users"}) // per-row fan-out
|
fetch({n_get, "https://api.users.dev/{{id}}", "profiles", .map = "users"}) // per-row fan-out
|
||||||
mustache("todo","todo_s", .map = "todos", .map_key = "todo_d") // render per row, collect into todo_s
|
mustache("todo","todo_s", .map = "todos", .map_key = "todo_d") // render per row, collect into todo_s
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ A task is a named, reusable pipeline defined inside a module with `task("name",
|
|||||||
// reusable, run inline via run_task("recount_todos"):
|
// reusable, run inline via run_task("recount_todos"):
|
||||||
task("recount_todos", { sqlite_query({"todos_db","recount"}) }, .accepts = {"user_id"});
|
task("recount_todos", { sqlite_query({"todos_db","recount"}) }, .accepts = {"user_id"});
|
||||||
// durable background job via dispatch("notify_new_todo") (needs #include <dispatch.h>):
|
// durable background job via dispatch("notify_new_todo") (needs #include <dispatch.h>):
|
||||||
task("notify_new_todo", { fetch({.url = "https://api.push.dev/notify", .method = n_post, .json = "{\"text\":\"New todo: {{title}}\"}"}) }, .accepts = {"title"});
|
task("notify_new_todo", { fetch({n_post, "https://api.push.dev/notify", .json = "{\"text\":\"New todo: {{title}}\"}"}) }, .accepts = {"title"});
|
||||||
// recurring on a schedule, no caller:
|
// recurring on a schedule, no caller:
|
||||||
task("daily_digest", { sqlite_query({"todos_db","digest"}), emit("digest_ready") }, .cron = "0 8 * * *");
|
task("daily_digest", { sqlite_query({"todos_db","digest"}), emit("digest_ready") }, .cron = "0 8 * * *");
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user