- Publish a typed web service with its parameter and stack options
- Inspect a script and answer with a JSON object
- execute, call and include: which environment?
- Generate the calls of a service for the team
- Read server parameters and trace a run in the logs
- Signed webhook receiver with replay protection
- Versioned API behind one router, deployed with merge
- Health check endpoint for the load balancer
- Bulk import endpoint with a line by line report
- An authentication guard shared with include
Publish a typed web service with its parameter and stack options
A shop needs a POST service that computes a price with VAT. The script stores the VAT rate as a server parameter, creates the script with typed and documented parameters, logs each call, sets its stack and delay options, then calls it once. It changes the server, so it is not run here.
#1. The VAT rate is a server parameter; if (parameter exist "SHOP_VAT_RATE") { parameter update "SHOP_VAT_RATE" "20"; parameter set_description "SHOP_VAT_RATE" "VAT rate in percent used by the shop"; } { parameter add "SHOP_VAT_RATE" "20" "VAT rate in percent used by the shop" false; }; #2. A POST script with typed parameters; script create post "shop.price.vat" true 1 (param (var "[net]" {type is_double [net]} "Net price" is_null:false is_empty:false "100") (var "[qty]" {type is_number [qty]} "Quantity" is_null:false is_empty:false "2") ;) "Gross price of a quantity of items" { -> "[rate]" (parameter get value "SHOP_VAT_RATE"); -> "[gross]" (math round (* (* [net] [qty]) (+ 1 (/ [rate] 100))) 2); log write (concat "price " [net] " x " [qty]) "OK" "qty" [qty]; [gross]; } "The gross amount"; #3. Stack and delay options; script set stack "shop.price.vat.post" 0 4 true; script set delay "shop.price.vat.post" 0 day {true;}; #4. First call; execute "shop.price.vat.post" "[net]" 19.90 "[qty]" 3;
- Change the rate later with
parameter update "SHOP_VAT_RATE" 5.5 - Give the URL to the front-end team with
script generate url "shop.price.vat.post"
Inspect a script and answer with a JSON object
Before wiring a front-end to a service, a developer checks that it exists, who may call it, which groups own it and what its parameters are. The script splits the name with atom functions, loads the definition, groups, users and parameters as JSON documents, then builds and edits the JSON answer with every insert, update and delete function.
#1. A script name is a dotted path ending with the method; -> "[name]" "addition.post"; -> "[method]" (atom get_last [name] "."); -> "[base]" (atom before_exclud [name] (atom size [name] ".") "."); -> "[ok]" (and (script exist [name]) (script is granted [name] "admin")); #2. Definition, groups, users and parameters as JSON; json load "rx_def" (script get [name]); json load by ref "rx_def" "/groups" "rx_grp"; -> "[admin]" (json load select "rx_usr" (script show users [name]) "/admin"); json load "rx_par" (script show params [name]); #3. Build the answer, starting from a draft; json load "rx_out" "{\"status\": \"draft\", \"tmp\": 0}"; json iobject "rx_out" "/" "script" [base] STR; json iobject "rx_out" "/" "method" [method] STR; json iobject "rx_out" "/" "granted" [ok] BOOL2; json iobject "rx_out" "/" "threads" (json select "rx_def" "/nb_in_thread") INT; json iobject "rx_out" "/" "groups" (json fields "rx_grp" "/") ARRAY; json iobject "rx_out" "/" "params" "[]" ARRAY; for (-> "[i]" 0) (< [i] (json count "rx_par" "/")) (++ "[i]") { json iarray "rx_out" "/params" (json select "rx_par" (concat "/[" [i] "]/name")) STR; }; #4. Edit arrays and fields; json sort "rx_out" "/params" "" DESC; json iarray "rx_out" "/params" 0 "[v0]" STR; json uarray "rx_out" "/params" 0 "[v9]" STR; json darray "rx_out" "/params" 0; json uobject "rx_out" "/" "status" "checked" STR; json dobject "rx_out" "/" "tmp"; json iobject "rx_out" "/" "checks" (concat "[" (json is object "rx_out" "/") ", " (json is array "rx_out" "/params") ", " (json exist "rx_usr") "]") ARRAY; json iobject "rx_out" "/" "loaded" (json show) ARRAY; -> "[answer]" (json doc "rx_out"); json unload_all; [answer];
{
"loaded": [
"rx_out",
"rx_par",
"rx_usr",
"rx_def",
"rx_grp"
],
"checks": [
1,
1,
1
],
"method": "post",
"threads": 1,
"groups": [
"mentdb"
],
"params": [
"[v2]",
"[v1]"
],
"script": "addition",
"status": "checked",
"granted": true
}- Build an HTML form from
script show params: one field per name, with its example and its type json unload "rx_def"frees one document,json unload_allfrees all of them
execute, call and include: which environment?
The same script is run three ways to see what happens to the variables of the caller. execute runs it in a new environment, call and include run it in the caller's environment, so the parameters they pass overwrite the caller's variables. Finally parallel runs three executions at the same time.
#1. The caller has its own v1; -> "[v1]" "mine"; -> "[r1]" (execute "addition.post" "[v1]" 10 "[v2]" 5); -> "[after_execute]" [v1]; #2. call and include share the environment of the caller; -> "[r2]" (call "addition.post" "[v1]" 20 "[v2]" 5); -> "[after_call]" [v1]; -> "[r3]" (include "addition.post" "[v1]" 30 "[v2]" 5); -> "[after_include]" [v1]; #3. Three executions in parallel; json load "rx_par" (parallel {execute "addition.post" "[v1]" 1 "[v2]" 1;} {execute "addition.post" "[v1]" 2 "[v2]" 2;} {execute "addition.post" "[v1]" 3 "[v2]" 3;}); -> "[p]" (concat (json select "rx_par" "/[0]/result") " " (json select "rx_par" "/[1]/result") " " (json select "rx_par" "/[2]/result") ", status " (json select "rx_par" "/[2]/status")); json unload "rx_par"; concat "execute: " [r1] ", v1 is still " [after_execute] [_n_] "call: " [r2] ", v1 is now " [after_call] [_n_] "include: " [r3] ", v1 is now " [after_include] [_n_] "parallel: " [p];
execute: 15, v1 is still mine call: 25, v1 is now 20 include: 35, v1 is now 30 parallel: 2 4 6, status OK
- Use
executefor services andincludefor small shared code blocks such as HTML parts - Run a script later and in the background with the code of
script generate stack
Generate the calls of a service for the team
A new service must be documented for the front-end and the other MQL developers. The script generates its HTTP URL with the headers, the MQL code to call it, include it, include it from a template and put it in the stack, and the first line of the create, update, merge and execute requests used to copy it to another server.
#1. The service to document; -> "[s]" "addition.post"; #2. For the front-end: method, headers and URL; -> "[doc]" (concat "== HTTP" [_n_] (script generate url [s])); #3. For MQL developers; -> "[doc]" (concat [doc] [_n_] "== call" [_n_] (script generate call [s])); -> "[doc]" (concat [doc] [_n_] "== include, first line: " (atom get_first (script generate include [s]) [_n_])); -> "[doc]" (concat [doc] [_n_] "== include from a template" [_n_] (script generate sub_include [s])); -> "[doc]" (concat [doc] [_n_] "== stack, first line: " (atom get_first (script generate stack [s]) [_n_])); #4. To copy the service to another server; -> "[doc]" (concat [doc] [_n_] "== create: " (atom get_first (script generate create [s]) [_n_]) [_n_] "== update: " (atom get_first (script generate update [s]) [_n_]) [_n_] "== merge: " (atom get_first (script generate merge [s]) [_n_]) [_n_] "== execute: " (atom get_first (script generate execute [s]) [_n_])); [doc];
== HTTP Method post Header x-token = (empty or not define the first time) x-user = admin (required the first time) x-password = ***** (required the first time) URL https://localhost:9999/api/addition?x-token=&x-user=&v1=&v2= == call call "addition.post" "[v1]" "example ..." "[v2]" "example ..." ; == include, first line: include "addition.post" == include from a template concat "include \"addition.post\" \"[v1]\" \"" (mql encode [v1]) "\" \"[v2]\" \"" (mql encode [v2]) "\" ;" == stack, first line: stack_execute "2026-11-01 16:24:31" "addition.post" == create: script create post "addition" false 1 == update: script update "addition.post" false 1 == merge: script merge "addition.post" false 1 == execute: execute "addition.post"
script generate delay "addition.post"gives its delay options as MQLscript generate executegives the same block withexecute, for an isolated run
Read server parameters and trace a run in the logs
A script must behave differently in DEV and PROD and leave a trace. It branches on the server mode with mode, reads a parameter, its description and its private flag, generates the update and merge requests to copy it elsewhere, writes two traces, reads them back and counts today's errors in the log database.
#1. Branch on the server mode; -> "[env]" (mode SERVER_MODE ("PROD") {"production"} ("DEV") {"development"} {"other"}); #2. A parameter, its description and its private flag; -> "[p]" "SERVER_MODE"; -> "[info]" (concat [p] " = " (parameter get value [p]) ", private " (parameter is_private [p]) ", exists " (parameter exist [p]) [_n_] (parameter get description [p])); #3. Requests to copy it to another server; -> "[copy]" (concat (parameter generate update [p]) [_n_] (parameter generate merge [p])); #4. Trace and read back; log trace (concat "rx_demo start in " [env]); log trace "rx_demo end"; -> "[last]" (log show 3); #5. Errors of the day in the log database; -> "[ko]" (log search "ko" null null null null (concat (date sysdate) " 00:00:00") (concat (date sysdate) " 23:59:59") DESC 1 50); json load "rx_ko" (string regexp_extract [ko] "(\{.*)$" 1); -> "[nb_ko]" (json count "rx_ko" "/data"); json unload "rx_ko"; concat "Server in " [env] [_n_] [info] [_n_] [copy] [_n_] "Last traces:" [_n_] (string replace [last] (date sysdate) "today") [_n_] "Errors today: " [nb_ko] ", current log id " (log current id) ", log retention " (log retention day) " days, archives of " (log archive size) " in " (log archive path);
Server in development SERVER_MODE = DEV, private 0, exists 1 Mode of the server (DEV, TEST, PROD...), shown in the administration. parameter update "SERVER_MODE" "DEV"; parameter merge "SERVER_MODE" "DEV"; Last traces: today 16:24:31.718+0400: rx_demo start in development today 16:24:31.718+0400: rx_demo end Errors today: 0, current log id 0, log retention 50 days, archives of 30 in archives/logs
- Follow one process and its children with
restricted show_pid - Write a business log line with a client key in the database with
log write
Signed webhook receiver with replay protection
A payment provider calls a POST web service with a JSON body and an HMAC-SHA256 signature. The service checks the signature with a private server parameter, ignores a retried event thanks to a node per event id, logs the payment and answers an HTTP-like code. The script plays a delivery, a retry and a forged body.
#1. The signing secret shared with the payment provider is a private parameter; parameter add "tuto_p4l_webhook_secret" "whsec_tuto_2026" "Tutorial: secret of the payment webhook" false; parameter set_private "tuto_p4l_webhook_secret" true; #2. The receiver: check the signature, refuse replays, then process the event; script create post "tuto.p4l.webhook.payment" false 1 (param (var "[body]" {type is_json [body]} "Raw JSON body of the webhook" is_null:false is_empty:false "{}") (var "[signature]" {type is_matches_regex [signature] "[0-9a-f]{64}"} "Header X-Signature, HMAC-SHA256 in hex" is_null:false is_empty:false "00") ;) "Receive a payment event from the provider" { if (not (equal [signature] (string hmac_sha256 [body] (parameter get value "tuto_p4l_webhook_secret")))) { exception ("401") ("invalid signature"); }; json load "rx_p4l_evt" [body]; -> "[id]" (json select "rx_p4l_evt" "/id"); -> "[amount]" (json select "rx_p4l_evt" "/amount"); json unload "rx_p4l_evt"; -> "[seen]" (concat "tuto_p4l_evt_" (string md5 [id])); if (node exist [seen]) { concat "{\"code\": 200, \"status\": \"duplicate ignored\", \"id\": \"" [id] "\"}"; } { node create [seen]; node iobject [seen] "/" "received_at" (date now) STR; log write (concat "payment " [id] " " [amount]) "OK" "event" [id]; concat "{\"code\": 201, \"status\": \"processed\", \"id\": \"" [id] "\", \"amount\": " [amount] "}"; }; } "JSON with the HTTP code"; #3. The provider sends an event, retries it, then an attacker forges one; -> "[evt]" "{\"id\": \"evt_7781\", \"type\": \"payment.succeeded\", \"amount\": 49.90}"; -> "[good]" (string hmac_sha256 [evt] "whsec_tuto_2026"); -> "[out]" ""; for (-> "[i]" 1) (<= [i] 3) (++ "[i]") { try { -> "[r]" (execute "tuto.p4l.webhook.payment.post" "[body]" (if (== [i] 3) {(string replace [evt] "49.90" "4990")} {[evt]}) "[signature]" [good]); } { -> "[r]" (concat "{\"code\": " (string regexp_extract [err] "([0-9]{3}): ([^\n]*)$" 1) ", \"error\": \"" (string regexp_extract [err] "([0-9]{3}): ([^\n]*)$" 2) "\"}"); } "[err]"; -> "[out]" (concat [out] "delivery " [i] ": " [r] [_n_]); }; #4. Cleanup of the tutorial objects; -> "[private]" (parameter is_private "tuto_p4l_webhook_secret"); node delete (concat "tuto_p4l_evt_" (string md5 "evt_7781")); script delete "tuto.p4l.webhook.payment.post"; parameter remove "tuto_p4l_webhook_secret"; concat [out] "secret was private: " [private] ", still on the server: " (parameter exist "tuto_p4l_webhook_secret");
delivery 1: {"code": 201, "status": "processed", "id": "evt_7781", "amount": 49.9}
delivery 2: {"code": 200, "status": "duplicate ignored", "id": "evt_7781"}
delivery 3: {"code": 401, "error": "invalid signature"}
secret was private: 1, still on the server: 0- Answer 200 on a duplicate, not an error: providers retry until they get a 2xx.
- Push the heavy processing to the stack with
stack (date now) ...and answer 202 at once.
Versioned API behind one router, deployed with merge
Deploy v1 and v2 of a price API and a public router with script merge, which creates or updates a script without failing if it exists. The router builds the name of the version script, answers 404 if it does not exist, and a hotfix of v1 is merged again without duplicates.
#1. Deploy v1 with merge: created if missing, updated if it exists; script merge "tuto.p4l.api.v1.price.get" false 1 (param (var "[net]" {type is_double [net]} "Net price" is_null:false is_empty:false "100") ;) "Price API v1: gross price as a number" { math round (* [net] 1.2) 2; } "A number"; #2. v2 answers JSON with the detail of the tax; script merge "tuto.p4l.api.v2.price.get" false 1 (param (var "[net]" {type is_double [net]} "Net price" is_null:false is_empty:false "100") ;) "Price API v2: JSON with net, vat and gross" { -> "[vat]" (math round (* [net] 0.2) 2); concat "{\"net\": " [net] ", \"vat\": " [vat] ", \"gross\": " (+ [net] [vat]) "}"; } "JSON"; #3. A router keeps one public URL and dispatches on the version; script merge "tuto.p4l.api.price.get" false 1 (param (var "[version]" {type is_matches_regex [version] "v[0-9]+"} "API version, v1 or v2" is_null:false is_empty:false "v2") (var "[net]" {type is_double [net]} "Net price" is_null:false is_empty:false "100") ;) "Public price API with a version" { -> "[target]" (concat "tuto.p4l.api." [version] ".price.get"); if (not (script exist [target])) {exception ("404") (concat "unknown version " [version]);}; execute [target] "[net]" [net]; } "The answer of the version"; #4. A hotfix of v1: merge again, the script is updated, not duplicated; script merge "tuto.p4l.api.v1.price.get" false 1 (param (var "[net]" {type is_double [net]} "Net price" is_null:false is_empty:false "100") ;) "Price API v1: gross price as a number, rounded to the cent" { math round (* [net] 1.2) 2; } "A number"; #5. Clients of each version call the same router; -> "[out]" ""; for (-> "[i]" 1) (<= [i] 3) (++ "[i]") { -> "[v]" (concat "v" [i]); try {-> "[r]" (execute "tuto.p4l.api.price.get" "[version]" [v] "[net]" 59.90);} {-> "[r]" (concat "error " (string regexp_extract [err] "([0-9]{3}: [^\n]*)$" 1));} "[err]"; -> "[out]" (concat [out] [v] " -> " [r] [_n_]); }; -> "[desc]" (json load select "rx_p4l_def" (script get "tuto.p4l.api.v1.price.get") "/desc"); script delete "tuto.p4l.api.v1.price.get"; script delete "tuto.p4l.api.v2.price.get"; script delete "tuto.p4l.api.price.get"; concat [out] "v1 after the hotfix: " [desc];
v1 -> 71.88
v2 -> {"net": 59.90, "vat": 11.98, "gross": 71.88}
v3 -> error 404: unknown version v3
v1 after the hotfix: Price API v1: gross price as a number, rounded to the cent- Put the merge commands in a deployment script: running it twice gives the same server.
script generate merge "tuto.p4l.api.v2.price.get"prints the merge command to copy a version to production.
Health check endpoint for the load balancer
A GET service answers the monitoring with a JSON health report: JVM and system memory, CPU, sessions, stack queue and errors, job scheduler, maintenance switches and version. Each problem is listed and the status becomes DEGRADED; the call uses a strict queue limit.
#1. A health endpoint for the load balancer and the monitoring; script create get "tuto.p4l.health" false 1 (param (var "[max_wait]" {type is_integer [max_wait] 5} "Max processes waiting in the stack before DEGRADED" is_null:false is_empty:false "50") ;) "Health check of the server, as JSON" { -> "[t0]" (date current_ms); json load "rx_p4l_health" "{\"checks\": {}, \"problems\": []}"; #Memory of the JVM: used/allocated/max in MB; -> "[jvm]" (metric current mem jvm); -> "[mem_pct]" (math round (* (/ (atom get [jvm] 1 "/") (atom get [jvm] 3 "/")) 100) 1); json iobject "rx_p4l_health" "/checks" "jvm_mem_used_pct" [mem_pct] NUM; json iobject "rx_p4l_health" "/checks" "system_mem_available_pct" (math round (* (/ (metric current available mem) (metric total mem)) 100) 1) NUM; json iobject "rx_p4l_health" "/checks" "cpu_jvm_pct" (metric current cpu jvm) NUM; json iobject "rx_p4l_health" "/checks" "sessions" (count sessions) INT; json iobject "rx_p4l_health" "/checks" "stack_waiting" (stack count_wait) INT; json iobject "rx_p4l_health" "/checks" "stack_errors" (stack count_error) INT; json iobject "rx_p4l_health" "/checks" "scheduler" (job scheduler status) STR; #Services stopped by a maintenance switch; json load "rx_p4l_mnt" (string regexp_extract (maintenance get) "(\{.*)$" 1); json parse_obj "rx_p4l_mnt" "/" "[service]" "[off]" { if (equal [off] "true") {json iarray "rx_p4l_health" "/problems" (concat [service] " in maintenance") STR;}; }; json unload "rx_p4l_mnt"; if (> [mem_pct] 90) {json iarray "rx_p4l_health" "/problems" "JVM memory above 90%" STR;}; if (> (stack count_wait) [max_wait]) {json iarray "rx_p4l_health" "/problems" "stack queue too long" STR;}; if (> (stack count_error) 0) {json iarray "rx_p4l_health" "/problems" "processes in error in the stack" STR;}; json iobject "rx_p4l_health" "/" "status" (if (== (json count "rx_p4l_health" "/problems") 0) {"UP"} {"DEGRADED"}) STR; json iobject "rx_p4l_health" "/" "version" (version) STR; json iobject "rx_p4l_health" "/" "functions" (function count) INT; json iobject "rx_p4l_health" "/" "checked_at" (date now) STR; json iobject "rx_p4l_health" "/" "duration_ms" (- (date current_ms) [t0]) INT; -> "[answer]" (json doc "rx_p4l_health"); json unload "rx_p4l_health"; [answer]; } "JSON with status UP or DEGRADED"; #2. The monitoring calls it with a strict queue limit; -> "[r]" (execute "tuto.p4l.health.get" "[max_wait]" 2); script delete "tuto.p4l.health.get"; [r];
{
"duration_ms": 3,
"checks": {
"scheduler": "Running...",
"sessions": 3,
"system_mem_available_pct": 27.1,
"jvm_mem_used_pct": 11.8,
"cpu_jvm_pct": 0.1,
"stack_waiting": 3,
"stack_errors": 0
},
"checked_at": "2026-11-01 18:25:25",
"functions": 1608,
"version": "4.2.3",
"problems": [
"stack queue too long"
],
"status": "DEGRADED"
}- Return a 503 with
exception ("503") ...when the status is DEGRADED, so the load balancer takes the node out. - Add
job scheduler statusandmetric current cpu systemthresholds that suit your server.
Bulk import endpoint with a line by line report
The front-end posts a CSV of customers to a POST service. Each line is cleaned (case, spaces), validated (e-mail, name, birth date in dd/MM/yyyy, plan in an enum) and either accepted in a normalised form or rejected with its line number and all its errors.
#1. A bulk import endpoint: the body is a CSV text, one customer per line; script create post "tuto.p4l.customers.import" false 1 (param (var "[csv]" {type is_varchar [csv] 100000} "CSV with header email;name;birth;plan" is_null:false is_empty:false "email;name;birth;plan") ;) "Validate and import customers, answer a line by line report" { json load "rx_p4l_imp" "{\"received\": 0, \"accepted\": [], \"rejected\": []}"; -> "[n]" (atom size [csv] [_n_]); #Line 1 is the header; for (-> "[i]" 2) (<= [i] [n]) (++ "[i]") { -> "[row]" (atom get [csv] [i] [_n_]); -> "[email]" (string lower (atom get_lrtrim [row] 1 ";")); -> "[name]" (string capitalize_words (string lower (atom get_lrtrim [row] 2 ";"))); -> "[birth]" (atom get_lrtrim [row] 3 ";"); -> "[plan]" (string lower (atom get_lrtrim [row] 4 ";")); -> "[why]" ""; if (not (type is_email [email])) {-> "[why]" (concat [why] "bad email, ");}; if (is empty [name]) {-> "[why]" (concat [why] "name missing, ");}; if (not (type is_valid_date [birth] "dd/MM/yyyy")) {-> "[why]" (concat [why] "bad birth date, ");}; if (not (type is_enum [plan] "free,pro,team")) {-> "[why]" (concat [why] "unknown plan, ");}; if (is empty [why]) { json iarray "rx_p4l_imp" "/accepted" (concat "{\"email\": \"" [email] "\", \"name\": \"" (string json_escape [name]) "\", \"birth\": \"" (date format [birth] "dd/MM/yyyy" "yyyy-MM-dd") "\", \"plan\": \"" [plan] "\"}") OBJ; } { json iarray "rx_p4l_imp" "/rejected" (concat "{\"line\": " [i] ", \"errors\": \"" (string substring [why] 0 (- (string length [why]) 2)) "\"}") OBJ; }; }; json uobject "rx_p4l_imp" "/" "received" (- [n] 1) INT; -> "[answer]" (json doc "rx_p4l_imp"); json unload "rx_p4l_imp"; [answer]; } "JSON report"; #2. The front-end posts a file of 5 customers; -> "[file]" (concat "email;name;birth;plan" [_n_] "ANA.Silva@Mail.com ; ana SILVA ; 18/06/1990 ; Pro" [_n_] "bob@mail ; Bob Martin ; 01/02/1985 ; free" [_n_] "chloe@mail.com ; ; 31/04/1992 ; team" [_n_] "dev@corp.io ; Dev Patel ; 07/11/2001 ; gold" [_n_] "eva@corp.io;Eva Durand;29/02/2000;TEAM"); -> "[r]" (execute "tuto.p4l.customers.import.post" "[csv]" [file]); script delete "tuto.p4l.customers.import.post"; #3. The answer summarised for the screen; json load "rx_p4l_r" [r]; -> "[sum]" (concat (json count "rx_p4l_r" "/accepted") " accepted, " (json count "rx_p4l_r" "/rejected") " rejected of " (json select "rx_p4l_r" "/received")); json unload "rx_p4l_r"; concat [sum] [_n_] [r];
2 accepted, 3 rejected of 5
{
"rejected": [
{
"line": 3,
"errors": "bad email"
},
{
"line": 4,
"errors": "name missing, bad birth date"
},
{
"line": 5,
"errors": "unknown plan"
}
],
"accepted": [
{
"name": "Ana Silva",
"birth": "1990-06-18",
"plan": "pro",
"email": "ana.silva@mail.com"
},
{
"name": "Eva Durand",
"birth": "2000-02-29",
"plan": "team",
"email": "eva@corp.io"
}
],
"received": 5
}- Insert the accepted array in one SQL transaction with
sql dmlandsql commit. - Over a few thousand lines, push the file to the stack and answer an import id at once.
An authentication guard shared with include
Every service of an API starts with the same guard: it finds the client by the SHA-256 of its API key in a node, counts its calls against a daily quota, and raises 401 or 429. Because the guard runs with include, the client name it sets is visible in the service.
#1. API clients in a node: hash of the key, name, quota and calls of the day; node create "tuto_p4l_clients"; node iobject "tuto_p4l_clients" "/" (string sha_256 "key-acme-42") (concat "{\"name\": \"ACME\", \"quota\": 2, \"calls\": 0}") OBJ; #2. A guard shared by every service: run with include, so it writes in the caller environment; script create exe "tuto.p4l.mw.guard" false 1 (param (var "[api_key]" {true} "Value of the x-api-key header" is_null:true is_empty:true "key") ;) "Authenticate the client and count its calls" { -> "[h]" (concat "/" (string sha_256 [api_key])); if (is null (node select "tuto_p4l_clients" [h])) {exception ("401") ("unknown api key");}; -> "[calls]" (+ (node select "tuto_p4l_clients" (concat [h] "/calls")) 1); if (> [calls] (node select "tuto_p4l_clients" (concat [h] "/quota"))) {exception ("429") ("quota of the day reached");}; node uobject "tuto_p4l_clients" [h] "calls" [calls] INT; -> "[client]" (node select "tuto_p4l_clients" (concat [h] "/name")); } "Sets [client] and [calls]"; #3. Two services start with the same guard line; script create get "tuto.p4l.mw.stock" false 1 (param (var "[api_key]" {true} "x-api-key" is_null:true is_empty:true "key") (var "[sku]" {true} "Product" is_null:false is_empty:false "BK-1");) "Stock of a product" { include "tuto.p4l.mw.guard.exe" "[api_key]" [api_key]; concat "{\"client\": \"" [client] "\", \"call\": " [calls] ", \"sku\": \"" [sku] "\", \"stock\": 14}"; } "JSON"; script create get "tuto.p4l.mw.price" false 1 (param (var "[api_key]" {true} "x-api-key" is_null:true is_empty:true "key") (var "[sku]" {true} "Product" is_null:false is_empty:false "BK-1");) "Price of a product" { include "tuto.p4l.mw.guard.exe" "[api_key]" [api_key]; concat "{\"client\": \"" [client] "\", \"call\": " [calls] ", \"sku\": \"" [sku] "\", \"price\": 19.9}"; } "JSON"; #4. Calls: good key twice, then over quota, then a wrong key; -> "[out]" ""; for (-> "[i]" 1) (<= [i] 4) (++ "[i]") { try { -> "[r]" (execute (if (== (math mod [i] 2) 1) {"tuto.p4l.mw.stock.get"} {"tuto.p4l.mw.price.get"}) "[api_key]" (if (== [i] 4) {"key-evil"} {"key-acme-42"}) "[sku]" "BK-1"); } { -> "[r]" (concat "HTTP " (string regexp_extract [err] "([0-9]{3}): ([^\n]*)$" 1) " " (string regexp_extract [err] "([0-9]{3}): ([^\n]*)$" 2)); } "[err]"; -> "[out]" (concat [out] "call " [i] ": " [r] [_n_]); }; #5. Cleanup; script delete "tuto.p4l.mw.stock.get"; script delete "tuto.p4l.mw.price.get"; script delete "tuto.p4l.mw.guard.exe"; node delete "tuto_p4l_clients"; [out];
call 1: {"client": "ACME", "call": 1, "sku": "BK-1", "stock": 14}
call 2: {"client": "ACME", "call": 2, "sku": "BK-1", "price": 19.9}
call 3: HTTP 429 quota of the day reached
call 4: HTTP 401 unknown api key- Never store the key itself: a leak of the node gives only hashes.
- Reset the counters every night with a job that runs
node uobjecton each client.
