Profiles / Programming
WS

Web service developer

A small REST resource with get, post, put and delete scripts: validation, JSON answers, codes, grants, URLs and API docs.

10 recipes on this page · open in the interactive view

Recipes
  1. POST: create an order with validation
  2. GET: one order, 404, or the list
  3. PUT: update a status along a workflow
  4. DELETE: idempotent delete with an audit trail
  5. Publish the four methods and document their URLs
  6. Generate an API description from the scripts
  7. API key and limits in server parameters
  8. Call the resource from another server
  9. Search with sort and pagination
  10. PUT with optional fields and optimistic locking
executed on MentDB

POST: create an order with validation

POST /api/tuto/sc/orders creates an order: typed parameters, a business rule, an id from a sequence, storage in a node and a 201 answer. Three calls show the mapping a client sees: 201, 422 and 400.

Pipeline
Script
admin
#1. Storage of the resource: a node and an id sequence;
node create "tuto_sc_orders";
sequence add "tuto_sc_order_id" 1000;
#2. POST /api/tuto/sc/orders: typed input, business rule, JSON answer;
script create post "tuto.sc.orders" false 1
	(param
		(var "[customer]" {type is_varchar [customer] 40} "Customer name" is_null:false is_empty:false "ACME")
		(var "[email]" {type is_email [email]} "Customer email" is_null:false is_empty:false "buyer@acme.test")
		(var "[amount]" {type is_decimal [amount] 10 2} "Order amount" is_null:false is_empty:false "120.50")
	;)
	"Create an order"
{
	if (<= [amount] 0) {exception ("422") ("amount must be positive");};
	-> "[id]" (concat "ORD-" (sequence increment "tuto_sc_order_id"));
	json load "tuto_sc_new" "{\"status\": \"new\"}";
	json iobject "tuto_sc_new" "/" "id" [id] STR;
	json iobject "tuto_sc_new" "/" "customer" [customer] STR;
	json iobject "tuto_sc_new" "/" "email" [email] STR;
	json iobject "tuto_sc_new" "/" "amount" [amount] NUM;
	node iobject "tuto_sc_orders" "/" [id] (json doc "tuto_sc_new") OBJ;
	json unload "tuto_sc_new";
	concat "{\"code\": 201, \"id\": \"" [id] "\"}";
} "JSON: code 201 and the new id";
#3. The same call the web server makes, plus error mapping like a client would see it;
-> "[out]" "";
for (-> "[i]" 1) (<= [i] 3) (++ "[i]") {
	try {
		-> "[r]" (execute "tuto.sc.orders.post" "[customer]" (atom get "ACME,Globex,Initech" [i] ",") "[email]" (atom get "buyer@acme.test,ap@globex.test,not-an-email" [i] ",") "[amount]" (atom get "120.50,-5,99" [i] ","));
	} {
		-> "[code]" (if (string contains [err] "is not valid") {400} {string regexp_extract [err] "([0-9]{3}): " 1});
		-> "[msg]" (if (== [code] 400) {concat "invalid parameter " (string regexp_extract [err] "variable '\[([a-z]+)\]'" 1)} {string regexp_extract [err] "[0-9]{3}: ([^\n]*)$" 1});
		-> "[r]" (concat "{\"code\": " [code] ", \"error\": \"" [msg] "\"}");
	} "[err]";
	-> "[out]" (concat [out] [r] [_n_]);
};
-> "[out]" (concat [out] "stored: " (node select "tuto_sc_orders" "/ORD-1001"));
#4. Clean up;
script delete "tuto.sc.orders.post";
node delete "tuto_sc_orders";
sequence remove "tuto_sc_order_id";
[out];
Result
mentdb
{"code": 201, "id": "ORD-1001"}
{"code": 422, "error": "amount must be positive"}
{"code": 400, "error": "invalid parameter email"}
stored: {
  "amount": 120.5,
  "id": "ORD-1001",
  "email": "buyer@acme.test",
  "status": "new",
  "customer": "ACME"
}
Go further
  • Through HTTP, a parameter error is answered by MentDB with status KO before your code runs.
  • Use type is_iban or type is_siret for B2B customers.
  • node show returns the whole resource for an admin page.
executed on MentDB

GET: one order, 404, or the list

GET /api/tuto/sc/orders returns one order by id, a 404 when it does not exist, or the sorted list of ids when the id is empty.

Pipeline
Script
admin
#1. Seed the resource with two orders;
node create "tuto_sc_orders";
node iobject "tuto_sc_orders" "/" "ORD-1001" "{\"id\": \"ORD-1001\", \"customer\": \"ACME\", \"amount\": 120.5, \"status\": \"paid\"}" OBJ;
node iobject "tuto_sc_orders" "/" "ORD-1002" "{\"id\": \"ORD-1002\", \"customer\": \"Globex\", \"amount\": 75, \"status\": \"new\"}" OBJ;
#2. GET /api/tuto/sc/orders?id=...: one order, 404, or the collection when id is empty;
script create get "tuto.sc.orders" false 1
	(param
		(var "[id]" {or (is null or empty [id]) (string matches [id] "ORD-[0-9]{4}")} "Order id, empty for the list" is_null:true is_empty:true "ORD-1001")
	;)
	"Read one order or the list"
{
	if (is null or empty [id]) {
		json load "tuto_sc_list" "{\"code\": 200, \"items\": []}";
		json load "tuto_sc_ids" (node fields "tuto_sc_orders" "/");
		for (-> "[i]" 0) (< [i] (json count "tuto_sc_ids" "/")) (++ "[i]") {
			-> "[k]" (json select "tuto_sc_ids" (concat "/[" [i] "]"));
			if (not (equal [k] "key")) {json iarray "tuto_sc_list" "/items" [k] STR;};
		};
		json sort "tuto_sc_list" "/items" "" ASC;
		json iobject "tuto_sc_list" "/" "count" (json count "tuto_sc_list" "/items") INT;
		-> "[res]" (json doc "tuto_sc_list");
		json unload "tuto_sc_ids";
		json unload "tuto_sc_list";
		[res];
	} {
		-> "[o]" (node select "tuto_sc_orders" (concat "/" [id]));
		if (is null [o]) {
			concat "{\"code\": 404, \"error\": \"order " [id] " not found\"}";
		} {
			concat "{\"code\": 200, \"order\": " [o] "}";
		};
	};
} "JSON with a code";
#3. Three requests;
-> "[out]" (concat
	"GET ?id=ORD-1002 -> " (string regexp_replace (execute "tuto.sc.orders.get" "[id]" "ORD-1002") "\s*\n\s*" " ") [_n_]
	"GET ?id=ORD-9999 -> " (execute "tuto.sc.orders.get" "[id]" "ORD-9999") [_n_]
	"GET ?id=         -> " (string regexp_replace (execute "tuto.sc.orders.get" "[id]" "") "\s*\n\s*" " "));
script delete "tuto.sc.orders.get";
node delete "tuto_sc_orders";
[out];
Result
mentdb
GET ?id=ORD-1002 -> {"code": 200, "order": { "amount": 75, "id": "ORD-1002", "customer": "Globex", "status": "new" }}
GET ?id=ORD-9999 -> {"code": 404, "error": "order ORD-9999 not found"}
GET ?id=         -> { "code": 200, "count": 2, "items": [ "ORD-1001", "ORD-1002" ] }
Go further
  • The node has a technical key field: skip it when you list.
  • Add [RESPONSE_HEADER_...] variables in the script to send HTTP headers.
  • See the search recipe for pagination.
executed on MentDB

PUT: update a status along a workflow

PUT /api/tuto/sc/orders changes the status of an order only along the allowed workflow (new to paid or cancelled, paid to shipped or cancelled) and keeps a history. Five calls give 200, 409, 200, 400 and 404.

Pipeline
Script
admin
#1. One order in the resource;
node create "tuto_sc_orders";
node iobject "tuto_sc_orders" "/" "ORD-1001" "{\"id\": \"ORD-1001\", \"customer\": \"ACME\", \"amount\": 120.5, \"status\": \"new\", \"history\": []}" OBJ;
#2. PUT /api/tuto/sc/orders: change the status, only along the allowed workflow;
script create put "tuto.sc.orders" false 1
	(param
		(var "[id]" {string matches [id] "ORD-[0-9]{4}"} "Order id" is_null:false is_empty:false "ORD-1001")
		(var "[status]" {type is_enum [status] "new,paid,shipped,cancelled"} "New status" is_null:false is_empty:false "paid")
	;)
	"Update the status of an order"
{
	-> "[path]" (concat "/" [id]);
	if (is null (node select "tuto_sc_orders" [path])) {exception ("404") (concat "order " [id] " not found");};
	-> "[from]" (node select "tuto_sc_orders" (concat [path] "/status"));
	-> "[allowed]" (switch [from] ("new") {"paid,cancelled"} ("paid") {"shipped,cancelled"} {""});
	if (not (type is_enum [status] [allowed])) {exception ("409") (concat "cannot go from " [from] " to " [status]);};
	node uobject "tuto_sc_orders" [path] "status" [status] STR;
	node iarray "tuto_sc_orders" (concat [path] "/history") (concat [from] " to " [status]) STR;
	concat "{\"code\": 200, \"id\": \"" [id] "\", \"status\": \"" [status] "\"}";
} "JSON with a code";
#3. A client that turns errors into codes;
-> "[out]" "";
for (-> "[i]" 1) (<= [i] 5) (++ "[i]") {
	-> "[id]" (atom get "ORD-1001,ORD-1001,ORD-1001,ORD-1001,ORD-7777" [i] ",");
	-> "[st]" (atom get "paid,new,shipped,delivered,paid" [i] ",");
	try {
		-> "[r]" (execute "tuto.sc.orders.put" "[id]" [id] "[status]" [st]);
	} {
		-> "[r]" (if (string contains [err] "is not valid") {concat "{\"code\": 400, \"error\": \"invalid status " [st] "\"}"} {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] "PUT " [id] " " [st] " -> " [r] [_n_]);
};
-> "[out]" (concat [out] "history: " (string regexp_replace (node select "tuto_sc_orders" "/ORD-1001/history") "\n\s*" ""));
script delete "tuto.sc.orders.put";
node delete "tuto_sc_orders";
[out];
Result
mentdb
PUT ORD-1001 paid -> {"code": 200, "id": "ORD-1001", "status": "paid"}
PUT ORD-1001 new -> {"code": 409, "error": "cannot go from paid to new"}
PUT ORD-1001 shipped -> {"code": 200, "id": "ORD-1001", "status": "shipped"}
PUT ORD-1001 delivered -> {"code": 400, "error": "invalid status delivered"}
PUT ORD-7777 paid -> {"code": 404, "error": "order ORD-7777 not found"}
history: ["new to paid","paid to shipped"]
Go further
  • type is_enum checks both the input and the transition.
  • node iarray appends to the history array.
  • Log the transitions with log write and a client key equal to the order id.
executed on MentDB

DELETE: idempotent delete with an audit trail

DELETE /api/tuto/sc/orders removes an order once (204), answers 404 the second time and refuses a shipped order (409). The logged script writes an audit line, found back with `log search` by client key.

Pipeline
Script
admin
#1. Two orders, one already shipped;
node create "tuto_sc_orders";
node iobject "tuto_sc_orders" "/" "ORD-1001" "{\"id\": \"ORD-1001\", \"status\": \"new\"}" OBJ;
node iobject "tuto_sc_orders" "/" "ORD-1002" "{\"id\": \"ORD-1002\", \"status\": \"shipped\"}" OBJ;
#2. DELETE /api/tuto/sc/orders: idempotent, refuses shipped orders;
script create delete "tuto.sc.orders" true 1
	(param
		(var "[id]" {string matches [id] "ORD-[0-9]{4}"} "Order id" is_null:false is_empty:false "ORD-1001")
	;)
	"Delete an order"
{
	-> "[st]" (node select "tuto_sc_orders" (concat "/" [id] "/status"));
	case
		(is null [st]) {"{\"code\": 404}";}
		(equal [st] "shipped") {"{\"code\": 409, \"error\": \"a shipped order cannot be deleted\"}";}
		{
			node dobject "tuto_sc_orders" "/" [id];
			log write (concat "order " [id] " deleted") "OK" "order" [id];
			"{\"code\": 204}";
		}
	;
} "JSON with a code";
#3. Delete twice, then the shipped one;
-> "[out]" (concat
	"DELETE ORD-1001 -> " (execute "tuto.sc.orders.delete" "[id]" "ORD-1001") [_n_]
	"DELETE ORD-1001 -> " (execute "tuto.sc.orders.delete" "[id]" "ORD-1001") [_n_]
	"DELETE ORD-1002 -> " (execute "tuto.sc.orders.delete" "[id]" "ORD-1002") [_n_]
	"left: " (string regexp_replace (node fields "tuto_sc_orders" "/") "\s+" ""));
#4. The audit trail written by the logged script;
-> "[audit]" (log search "ok" "tuto.sc.orders.delete" "order" "ORD-1001" null (concat (date sysdate) " 00:00:00") (concat (date sysdate) " 23:59:59") DESC 1 5);
json load "tuto_sc_audit" (string regexp_extract [audit] "(\{.*)$" 1);
-> "[out]" (concat [out] [_n_] "audit: " (json select "tuto_sc_audit" "/data/[0]/msg") ", by " (json select "tuto_sc_audit" "/data/[0]/script") ", status " (json select "tuto_sc_audit" "/data/[0]/status"));
json unload "tuto_sc_audit";
script delete "tuto.sc.orders.delete";
node delete "tuto_sc_orders";
[out];
Result
mentdb
DELETE ORD-1001 -> {"code": 204}
DELETE ORD-1001 -> {"code": 404}
DELETE ORD-1002 -> {"code": 409, "error": "a shipped order cannot be deleted"}
left: ["ORD-1002","key"]
audit: order ORD-1001 deleted, by tuto.sc.orders.delete, status ok
Go further
  • log search returns a JSON table: data, columns, column_types.
  • Filter by script and client value to audit one order.
  • Use status KO in log write for refused deletes.
executed on MentDB

Publish the four methods and document their URLs

The four scripts of the resource are granted to the existing group sample in one request, checked one by one, and their HTTP URL and headers are generated with the login masked; then they are unpublished and deleted.

Pipeline
Script
admin
#1. The four methods of the resource, bodies kept short here;
script create get "tuto.sc.orders" false 1 (param (var "[id]" {true} "Order id" is_null:true is_empty:true "ORD-1001");) "Read" {"{\"code\": 200}";} "JSON";
script create post "tuto.sc.orders" false 1 (param (var "[customer]" {type is_varchar [customer] 40} "Customer" is_null:false is_empty:false "ACME") (var "[amount]" {type is_double [amount]} "Amount" is_null:false is_empty:false "120.5");) "Create" {"{\"code\": 201}";} "JSON";
script create put "tuto.sc.orders" false 1 (param (var "[id]" {true} "Order id" is_null:false is_empty:false "ORD-1001") (var "[status]" {type is_enum [status] "new,paid,shipped"} "Status" is_null:false is_empty:false "paid");) "Update" {"{\"code\": 200}";} "JSON";
script create delete "tuto.sc.orders" false 1 (param (var "[id]" {true} "Order id" is_null:false is_empty:false "ORD-1001");) "Delete" {"{\"code\": 204}";} "JSON";
#2. Publish all of them to the existing group sample in one request;
-> "[g]" "sample";
-> "[pub]" (group grant_all script "tuto.sc.orders." [g]);
-> "[check]" "";
-> "[doc]" "";
for (-> "[i]" 1) (<= [i] 4) (++ "[i]") {
	-> "[s]" (concat "tuto.sc.orders." (atom get "get,post,put,delete" [i] ","));
	-> "[check]" (concat [check] " " (atom get "get,post,put,delete" [i] ",") "=" (group is granted script [s] [g]));
	#3. HTTP documentation of each method, secrets masked;
	-> "[u]" (script generate url [s]);
	-> "[u]" (string regexp_replace [u] "x-user = [^ \n]+" "x-user = <login>");
	-> "[u]" (string regexp_replace [u] "x-user=[^&]*" "x-user=<login>");
	-> "[u]" (string regexp_replace [u] "x-password=[^&]*" "x-password=<secret>");
	-> "[doc]" (concat [doc] [_n_] (string upper (atom get "get,post,put,delete" [i] ",")) " " (string regexp_extract [u] "URL\s+(\S+)" 1));
};
-> "[headers]" (string regexp_extract [u] "(Header[\s\S]*)URL" 1);
#4. Unpublish and delete;
-> "[unpub]" (group ungrant_all script "tuto.sc.orders." [g]);
-> "[after]" (group is granted script "tuto.sc.orders.get" [g]);
script delete "tuto.sc.orders.get";
script delete "tuto.sc.orders.post";
script delete "tuto.sc.orders.put";
script delete "tuto.sc.orders.delete";
concat [pub] " in " [g] ":" [check] [doc] [_n_] (string trim [headers]) [_n_] [unpub] " Get still in " [g] ": " [after];
Result
mentdb
Scripts granted with successful. in sample: get=1 post=1 put=1 delete=1
GET https://localhost:9999/api/tuto/sc/orders?x-token=&x-user=<login>&id=
POST https://localhost:9999/api/tuto/sc/orders?x-token=&x-user=<login>&customer=&amount=
PUT https://localhost:9999/api/tuto/sc/orders?x-token=&x-user=<login>&id=&status=
DELETE https://localhost:9999/api/tuto/sc/orders?x-token=&x-user=<login>&id=
Header x-token = (empty or not define the first time)
  x-user = <login> (required the first time)
  x-password = ***** (required the first time)
Scripts ungranted with successful. Get still in sample: 0
Go further
  • The URL is the script name without the method; the HTTP method selects the script.
  • The first call sends x-user and x-password, the next ones the x-token.
  • Never paste a real password in the generated doc: mask it with string regexp_replace.
executed on MentDB

Generate an API description from the scripts

An API description of the resource is built from the scripts themselves: summary from the description, and one line per parameter with its name, required flag, label, check and example.

Pipeline
Script
admin
#1. Two methods of the resource, with documented parameters;
script create get "tuto.sc.orders" false 1
	(param
		(var "[id]" {or (is null or empty [id]) (string matches [id] "ORD-[0-9]{4}")} "Order id, empty for the list" is_null:true is_empty:true "ORD-1001")
	;)
	"Read one order or the list" {"{}";} "JSON: code, order or items";
script create post "tuto.sc.orders" false 1
	(param
		(var "[customer]" {type is_varchar [customer] 40} "Customer name" is_null:false is_empty:false "ACME")
		(var "[email]" {type is_email [email]} "Customer email" is_null:false is_empty:false "buyer@acme.test")
		(var "[amount]" {type is_decimal [amount] 10 2} "Order amount" is_null:false is_empty:false "120.50")
	;)
	"Create an order" {"{}";} "JSON: code 201 and id";
#2. Build an API description from the scripts themselves;
json load "tuto_sc_api" "{\"title\": \"Tutorial orders API\", \"endpoint\": \"/api/tuto/sc/orders\", \"methods\": {}}";
-> "[methods]" "get,post";
for (-> "[m]" 1) (<= [m] (atom size [methods] ",")) (++ "[m]") {
	-> "[method]" (atom get [methods] [m] ",");
	-> "[s]" (concat "tuto.sc.orders." [method]);
	json load "tuto_sc_def" (script get [s]);
	json load "tuto_sc_par" (script show params [s]);
	json load "tuto_sc_op" "{\"parameters\": []}";
	json iobject "tuto_sc_op" "/" "summary" (json select "tuto_sc_def" "/desc") STR;
	for (-> "[i]" 0) (< [i] (json count "tuto_sc_par" "/")) (++ "[i]") {
		-> "[p]" (concat "/[" [i] "]");
		-> "[check]" (json select "tuto_sc_par" (concat [p] "/type"));
		json iarray "tuto_sc_op" "/parameters" (concat (string regexp_extract (json select "tuto_sc_par" (concat [p] "/name")) "\[(.*)\]" 1)
			(if (equal (json select "tuto_sc_par" (concat [p] "/isNull")) "0") {" (required)"} {" (optional)"})
			": " (json select "tuto_sc_par" (concat [p] "/desc"))
			", check " (string regexp_replace [check] ";$" "")
			", e.g. " (json select "tuto_sc_par" (concat [p] "/example"))) STR;
	};
	json iobject "tuto_sc_api" "/methods" [method] (json doc "tuto_sc_op") OBJ;
	json unload "tuto_sc_op";
	json unload "tuto_sc_par";
	json unload "tuto_sc_def";
};
-> "[api]" (json doc "tuto_sc_api");
json unload "tuto_sc_api";
script delete "tuto.sc.orders.get";
script delete "tuto.sc.orders.post";
[api];
Result
mentdb
{
  "endpoint": "/api/tuto/sc/orders",
  "methods": {
    "post": {
      "summary": "Create an order",
      "parameters": [
        "customer (required): Customer name, check type is_varchar [customer] 40, e.g. ACME",
        "email (required): Customer email, check type is_email [email], e.g. buyer@acme.test",
        "amount (required): Order amount, check type is_decimal [amount] 10 2, e.g. 120.50"
      ]
    },
    "get": {
      "summary": "Read one order or the list",
      "parameters": [
        "id (optional): Order id, empty for the list, check or (is null or empty [id]) (string matches [id] \"ORD-[0-9]{4}\"), e.g. ORD-1001"
      ]
    }
  },
  "title": "Tutorial orders API"
}
Go further
  • Publish the JSON with an .exe or get script so the front-end can fetch it.
  • Add the URL of each method with script generate url.
  • The check is the MQL expression: translate common ones to JSON Schema formats.
executed on MentDB

API key and limits in server parameters

A partner service checks a private API key and a maximum amount stored as server parameters. Four calls give 201, 401, 413 and, after the limit is raised, 201; the settings are described without leaking the key.

Pipeline
Script
admin
#1. API settings as server parameters: a public limit and a private key;
parameter add "tuto_sc_max_amount" "5000" "Tutorial: max amount of an order" false;
parameter add "tuto_sc_api_key" (string uuid) "Tutorial: key of the partner" false;
parameter set_private "tuto_sc_api_key" true;
parameter set_description "tuto_sc_max_amount" "Tutorial: max amount of one order in EUR";
#2. The service checks the key and the limit at each call;
script create post "tuto.sc.partner.order" false 1
	(param
		(var "[key]" {type is_uuid [key]} "Partner key, sent as x-api-key" is_null:false is_empty:false "00000000-0000-0000-0000-000000000000")
		(var "[amount]" {type is_double [amount]} "Order amount" is_null:false is_empty:false "100")
	;)
	"Partner order with key and limit"
{
	if (not (equal [key] (parameter get value "tuto_sc_api_key"))) {exception ("401") ("bad api key");};
	if (> [amount] (parameter get value "tuto_sc_max_amount")) {exception ("413") (concat "max amount is " (parameter get value "tuto_sc_max_amount"));};
	concat "{\"code\": 201, \"amount\": " [amount] "}";
} "JSON";
#3. Calls: good key, bad key, amount too high, then after raising the limit;
-> "[good]" (parameter get value "tuto_sc_api_key");
-> "[out]" "";
for (-> "[i]" 1) (<= [i] 4) (++ "[i]") {
	if (== [i] 4) {parameter update "tuto_sc_max_amount" "8000";};
	try {
		-> "[r]" (execute "tuto.sc.partner.order.post" "[key]" (if (== [i] 2) {(string uuid)} {[good]}) "[amount]" (if (>= [i] 3) {6500} {1200}));
	} {
		-> "[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] "call " [i] ": " [r] [_n_]);
};
#4. Describe the settings without leaking the key;
-> "[out]" (concat [out]
	(parameter get description "tuto_sc_max_amount") " = " (parameter get value "tuto_sc_max_amount") [_n_]
	"key exists " (parameter exist "tuto_sc_api_key") ", private " (parameter is_private "tuto_sc_api_key") [_n_]
	(parameter generate update "tuto_sc_max_amount") [_n_]
	(string regexp_replace (parameter generate merge "tuto_sc_api_key") "\"[0-9a-f-]{36}\"" "\"*****\""));
parameter remove "tuto_sc_api_key";
parameter remove "tuto_sc_max_amount";
script delete "tuto.sc.partner.order.post";
[out];
Result
mentdb
call 1: {"code": 201, "amount": 1200}
call 2: {"code": 401, "error": "bad api key"}
call 3: {"code": 413, "error": "max amount is 5000"}
call 4: {"code": 201, "amount": 6500}
Tutorial: max amount of one order in EUR = 8000
key exists 1, private 1
parameter update "tuto_sc_max_amount" "8000";
parameter merge "tuto_sc_api_key" "*****";
Go further
  • In HTTP, read the key from the [REQUEST_HEADER_X-API-KEY] variable instead of a parameter.
  • parameter generate update prints the value: never use it on a secret without masking.
  • parameter remove returns the old value, do not print it.
needs your systems

Call the resource from another server

On a client MentDB server, the orders API is called over HTTPS: POST with URL-encoded query parameters and the login from local parameters, check of the MentDB envelope, then a GET of the new order with the returned token. Not executed here: it needs a remote server.

Pipeline
Script
admin
#1. On the CLIENT server: the URL and the login come from its own parameters;
-> "[url]" (parameter get value "ORDERS_API_URL");
-> "[headers]" (concat "{\"x-user\": \"" (parameter get value "ORDERS_API_LOGIN") "\", \"x-password\": \"" (parameter get value "ORDERS_API_PASSWORD") "\"}");
#2. POST: parameters go in the query string, URL-encoded;
-> "[query]" (concat "?customer=" (string url_encode "ACME Corp") "&email=" (string url_encode "buyer@acme.test") "&amount=120.50");
json load "tuto_sc_resp" (rest https_json_post (concat [url] [query]) [headers] "[]" "{}");
#3. MentDB wraps the answer: status, token, result;
if (not (equal (json select "tuto_sc_resp" "/status") "OK")) {
	-> "[msg]" (json select "tuto_sc_resp" "/result");
	json unload "tuto_sc_resp";
	exception ("502") (concat "orders API failed: " [msg]);
};
-> "[id]" (json select "tuto_sc_resp" "/result/id");
-> "[token]" (json select "tuto_sc_resp" "/token");
json unload "tuto_sc_resp";
#4. Next calls reuse the token instead of the password: GET the new order;
json load "tuto_sc_http" (rest https get [url] (concat "id=" [id]) (concat "{\"x-token\": \"" [token] "\"}") "[]");
json load "tuto_sc_resp" (json select "tuto_sc_http" "/reponse");
-> "[order]" (json select "tuto_sc_resp" "/result/order");
json unload "tuto_sc_resp";
json unload "tuto_sc_http";
concat "created " [id] ": " [order];
Result
This recipe uses your own connections (database, SFTP, SMTP...). Create them with the CM functions, then run it on your server.
Go further
  • The MentDB envelope holds MentDB, status, session, time, user, timestamp, token and result.
  • Raise your own exception when status is not OK, so the stack can replay the run.
  • Store the token for the session instead of sending the password each time.
executed on MentDB

Search with sort and pagination

GET /api/tuto/sc/orders/search sorts the orders by amount or customer and returns one page with total and page count. A page size over 50 is refused by the parameter check.

Pipeline
Script
admin
#1. Five orders in the resource;
node create "tuto_sc_orders";
-> "[seed]" "ORD-1001:ACME:120.5,ORD-1002:Globex:75,ORD-1003:Initech:310,ORD-1004:Umbrella:42.9,ORD-1005:Hooli:199";
for (-> "[i]" 1) (<= [i] (atom size [seed] ",")) (++ "[i]") {
	-> "[o]" (atom get [seed] [i] ",");
	node iobject "tuto_sc_orders" "/" (atom get [o] 1 ":") (concat "{\"id\": \"" (atom get [o] 1 ":") "\", \"customer\": \"" (atom get [o] 2 ":") "\", \"amount\": " (atom get [o] 3 ":") "}") OBJ;
};
#2. GET /api/tuto/sc/orders/search: sort and paginate;
script create get "tuto.sc.orders.search" false 1
	(param
		(var "[sort]" {type is_enum [sort] "amount,customer"} "Sort field" is_null:false is_empty:false "amount")
		(var "[order]" {type is_enum [order] "ASC,DESC"} "Sort order" is_null:false is_empty:false "DESC")
		(var "[page]" {and (type is_integer [page] 3) (>= [page] 1)} "Page, from 1" is_null:false is_empty:false "1")
		(var "[size]" {and (type is_integer [size] 2) (<= [size] 50)} "Rows per page, 50 max" is_null:false is_empty:false "2")
	;)
	"Search the orders"
{
	json load "tuto_sc_all" "[]";
	json load "tuto_sc_ids" (node fields "tuto_sc_orders" "/");
	for (-> "[i]" 0) (< [i] (json count "tuto_sc_ids" "/")) (++ "[i]") {
		-> "[k]" (json select "tuto_sc_ids" (concat "/[" [i] "]"));
		if (equal [k] "key") {continue;};
		json iarray "tuto_sc_all" "/" (node select "tuto_sc_orders" (concat "/" [k])) OBJ;
	};
	json sort "tuto_sc_all" "/" [sort] [order];
	-> "[total]" (json count "tuto_sc_all" "/");
	json load "tuto_sc_page" (concat "{\"code\": 200, \"page\": " [page] ", \"size\": " [size] ", \"total\": " [total] ", \"pages\": " (math ceil (/ [total] [size])) ", \"items\": []}");
	for (-> "[i]" (* (- [page] 1) [size])) (< [i] (* [page] [size])) (++ "[i]") {
		if (>= [i] [total]) {break;};
		json iarray "tuto_sc_page" "/items" (concat (json select "tuto_sc_all" (concat "/[" [i] "]/id")) " " (json select "tuto_sc_all" (concat "/[" [i] "]/customer")) " " (json select "tuto_sc_all" (concat "/[" [i] "]/amount"))) STR;
	};
	-> "[res]" (json doc "tuto_sc_page");
	json unload "tuto_sc_page";
	json unload "tuto_sc_ids";
	json unload "tuto_sc_all";
	[res];
} "JSON page";
#3. Page 1 by amount, last page, then a refused page size;
-> "[out]" (concat
	(string regexp_replace (execute "tuto.sc.orders.search.get" "[sort]" "amount" "[order]" "DESC" "[page]" 1 "[size]" 2) "\n\s*" " ") [_n_]
	(string regexp_replace (execute "tuto.sc.orders.search.get" "[sort]" "customer" "[order]" "ASC" "[page]" 3 "[size]" 2) "\n\s*" " "));
try {execute "tuto.sc.orders.search.get" "[sort]" "amount" "[order]" "ASC" "[page]" 1 "[size]" 500;} {-> "[out]" (concat [out] [_n_] "{\"code\": 400, \"error\": \"invalid " (string regexp_extract [err] "variable '\[([a-z]+)\]'" 1) "\"}");} "[err]";
script delete "tuto.sc.orders.search.get";
node delete "tuto_sc_orders";
[out];
Result
mentdb
{ "total": 5, "code": 200, "pages": 3, "size": 2, "page": 1, "items": [ "ORD-1003 Initech 310", "ORD-1005 Hooli 199" ] }
{ "total": 5, "code": 200, "pages": 3, "size": 2, "page": 3, "items": [ "ORD-1004 Umbrella 42.9" ] }
{"code": 400, "error": "invalid size"}
Go further
  • Combine checks in a parameter: and (type is_integer [size] 2) (<= [size] 50).
  • break and continue keep the loops short.
  • For thousands of rows, store the orders in SQL and paginate with LIMIT.
executed on MentDB

PUT with optional fields and optimistic locking

A PUT on a customer accepts null for the fields the client does not change (is_null:true), keeps the stored values for them, and refuses a stale version with a 409. The script generator shows the call to give to the client team.

Pipeline
Script
admin
#1. A customer resource;
node create "tuto_p4m_customers";
node iobject "tuto_p4m_customers" / C42 "{\"email\": \"lea@example.com\", \"phone\": \"0262 41 00 00\", \"plan\": \"free\", \"version\": 1}" OBJ;
#2. PUT with optional fields: a null field keeps the stored value;
script create put "tuto.p4m.customer" false 1
	(param
		(var "[id]" {string matches [id] "C[0-9]+"} "Customer id" is_null:false is_empty:false "C42")
		(var "[email]" {or (is null [email]) (type is_email [email])} "New email, null to keep it" is_null:true is_empty:false "lea@example.com")
		(var "[plan]" {or (is null [plan]) (type is_enum [plan] "free,pro,team")} "New plan, null to keep it" is_null:true is_empty:false "pro")
		(var "[version]" {type is_integer [version] 6} "Version read by the client" is_null:false is_empty:false "1")
	;)
	"Partial update of a customer with optimistic locking"
{
	-> "[p]" (concat "/" [id]);
	if (is null (node select "tuto_p4m_customers" [p])) {exception ("404") (concat [id] " not found");};
	-> "[cur]" (node select "tuto_p4m_customers" (concat [p] "/version"));
	if (not (equal [cur] [version])) {exception ("409") (concat "stale version " [version] ", current is " [cur]);};
	-> "[changed]" "";
	if (is not null [email]) {node uobject "tuto_p4m_customers" [p] email [email] STR; -> "[changed]" (concat [changed] "email ");};
	if (is not null [plan]) {node uobject "tuto_p4m_customers" [p] plan [plan] STR; -> "[changed]" (concat [changed] "plan ");};
	node uobject "tuto_p4m_customers" [p] version (+ [cur] 1) INT;
	concat "{\"code\": 200, \"changed\": \"" (string trim [changed]) "\", \"version\": " (+ [cur] 1) "}";
} "JSON with a code";
#3. What the generator proposes to the client;
-> "[call]" (script generate execute "tuto.p4m.customer.put");
#4. Three clients: plan only, email only with an old version, bad plan;
-> "[out]" "";
for (-> "[i]" 1) (<= [i] 3) (++ "[i]") {
	try {
		-> "[r]" (case
			((equal [i] 1)) {execute "tuto.p4m.customer.put" "[id]" "C42" "[email]" null "[plan]" "pro" "[version]" 1}
			((equal [i] 2)) {execute "tuto.p4m.customer.put" "[id]" "C42" "[email]" "lea.payet@example.com" "[plan]" null "[version]" 1}
			{execute "tuto.p4m.customer.put" "[id]" "C42" "[email]" null "[plan]" "gold" "[version]" 2}
		;);
	} {
		-> "[r]" (if (string contains [err] "is not valid") {"{\"code\": 400}"} {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] "client " [i] " -> " [r] [_n_]);
};
-> "[out]" (concat [call] [_n_] [out] "stored: " (string regexp_replace (node select "tuto_p4m_customers" /C42) "\n\s*" ""));
script delete "tuto.p4m.customer.put";
node delete "tuto_p4m_customers";
[out]
Result
mentdb
execute "tuto.p4m.customer.put"
	"[id]" "C42"
	"[email]" "lea@example.com"
	"[plan]" "pro"
	"[version]" "1"
;
client 1 -> {"code": 200, "changed": "plan", "version": 2}
client 2 -> {"code": 409, "error": "stale version 1, current is 2"}
client 3 -> {"code": 400}
stored: {"phone": "0262 41 00 00","plan": "pro","version": 2,"email": "lea@example.com"}
Go further
  • The validation of a nullable parameter must accept null itself: {or (is null [plan]) (type is_enum [plan] "free,pro,team")}.
  • Store the version with INT, not NUM: NUM gives 2.0 and a string comparison with the 2 of the client fails.

Also in Programming

MB
MQL beginner
Variables, operators, if, loops and your first try/exception: learn MQL step by step.
10 recipes
MD
MQL developer
Atom lists, data types, constants, translations, session variables and error patterns.
10 recipes
BW
Backend & web service developer
Typed scripts, execute / call / include, generated calls and URLs, parameters and logs.
10 recipes
DT
Date & time developer
Calendars, business days, time zones, ISO and epoch, durations, quarters and week numbers.
10 recipes
SE
String & encoding developer
Cleaning, case styles, regex, URL/HTML/Base64 encodings, number bases and encryption.
10 recipes
DV
Data validation developer
IBAN, BIC, SIRET, Luhn, email, IP, UUID, JSON and date checks gathered in a validation report.
10 recipes
JD
JSON & document store developer
In-memory JSON documents, persistent nodes and sequences: build, walk, sort, clean up.
10 recipes
SD
Script developer
Full script lifecycle in the MQL editor: typed parameters, read back, generate, patch, deploy, copy, grant, delete.
10 recipes
EM
Execution modes
execute, call, include, sub_include, parallel, stack: which environment, which checks, which logs.
11 recipes
AD
Async developer
Asynchronous processing with the stack: push runs now or later, priorities and parallel limits, errors and replays, flows and cleanup.
10 recipes
SD
Scheduler developer
Cron jobs and delayed runs: add, resume, pause, update and remove jobs, prove they ran, and postpone stack runs with a delay condition.
10 recipes
ED
Event developer
Event-driven scripts: file watchers that push each file event to the stack, validation, archiving, errors and safe restarts.
10 recipes
CC
Configuration & connections
Parameters (private, described, locked, deployed), environment variables, sessions and connections used by your scripts.
12 recipes
VD
Versioning & deployment
Group your scripts, commit, compare, roll back, export, import and package a release.
10 recipes
DE
Debugging & error handling
Coded errors with try/exception, logs with correlation ids, timing, metrics and defensive checks.
10 recipes
SD
SQL developer in MQL
Connections, DDL and DML with sql encode, transactions, cursors, parse, and exports to JSON, CSV, Excel and PDF on a real local database. Run the recipes in order: the first one creates the tuto_db_h2 database.
10 recipes
CD
CRUD & data API developer
Generate SCRUD scripts from a table, patch and install them, write typed read and write APIs, check data quality and manage connections per environment. Uses the tuto_db_h2 database of the SQL developer recipes; the last recipe removes it.
10 recipes
WA
Web application developer
Create a web app, its host name, pages, ajax forms, menu and grants, then remove it all.
10 recipes
AI
Algorithms in MQL
Sorting, searching, recursion, data structures and number algorithms: MQL is a real programming language.
10 recipes
CF
Control flow
if, if force, case, switch, mode, for, while, repeat, break, continue, parallel and synchronized on real cases.
10 recipes
TM
The MQL language in depth
Data types and null, booleans, comments, variables and scope, eval and mql blocks, coded errors and try.
10 recipes
SU
Screen & UI developer (sajax)
Build the screens of MentDB web apps in MQL: Bootstrap grids, forms and every control, ajax buttons, alerts, refreshes and charts.
10 recipes