JavaScript / Node.js
Nessuna dipendenza esterna: usa fetch nativo (Node.js 20+).
Avvio rapido
export ECUBE_BASE_URL="https://api.e-cube.it"
export ECUBE_USERNAME="<USERNAME>"
export ECUBE_PASSWORD="<PASSWORD>"
node samples/node/esempio.mjs
| Funzione | Endpoint |
|---|---|
getToken() | POST /Token |
readAll(resource, recordSelection, pageSize) | GET /api/resources/{R} paginato |
readChanges(resource, since, pageSize) | GET /api/sync/{R}?LastUtcRun= |
query(resource, filters, sort) | POST /api/resources/{R}/query |
sync(resource, records, batchSize) | POST /api/sync/{R} |
request(method, path, body) | Qualsiasi chiamata autenticata |
Esempio
samples/node/esempio.mjs
// Esempio in SOLA LETTURA: node samples/node/esempio.mjs
import { readAll, query, request } from "./ecube.mjs";
let n = 0;
for await (const f of readAll("Famiglia", undefined, 5)) {
console.log(f.cod_famiglia, f.des_famiglia);
if (++n === 5) break;
}
const anno = new Date().getUTCFullYear();
const documenti = await request("GET", `/api/resources/MCPDocumentiT?PageSize=1&PageNumber=1&RecordSelection=${
encodeURIComponent(`num_anno=${anno}`)}`);
console.log(`Documenti ${anno}:`, documenti[0]?.TotalRowsCount ?? 0);
const q = await query("MCPClienti", [{ FieldName: "des_ragsoc", Operator: "contains", Value: "S" }],
[{ FieldName: "des_ragsoc", Direction: "asc" }], 5);
console.log("Query: risposta di tipo", Array.isArray(q) ? `array (${q.length})` : typeof q);
Modulo ecube.mjs
samples/node/ecube.mjs
// Client minimo per le WebAPI E-CUBE (Node.js 20+, fetch nativo). Vedi https://docs.e-cube.it
// Variabili d'ambiente: ECUBE_BASE_URL (facoltativa), ECUBE_USERNAME, ECUBE_PASSWORD
const BASE_URL = (process.env.ECUBE_BASE_URL ?? "https://api.e-cube.it").replace(/\/$/, "");
let token = null;
let tokenExpires = 0;
export async function getToken(force = false) {
if (!force && token && Date.now() < tokenExpires) return token;
const resp = await fetch(`${BASE_URL}/Token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "password",
username: process.env.ECUBE_USERNAME ?? "",
password: process.env.ECUBE_PASSWORD ?? "",
}),
});
if (!resp.ok) throw new Error(`Token: HTTP ${resp.status} ${await resp.text()}`);
const body = await resp.json();
token = body.access_token;
tokenExpires = Date.now() + (body.expires_in ?? 3600) * 900; // 90% della durata
return token;
}
const transient = s => s === 408 || s === 429 || s >= 500;
const sleep = ms => new Promise(r => setTimeout(r, ms));
/** Richiesta con Bearer token, un rinnovo su 401 e retry con attesa crescente sugli errori temporanei. */
export async function request(method, path, body, maxAttempts = 4) {
let refreshed = false;
for (let attempt = 1; ; attempt++) {
let resp;
try {
resp = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
Authorization: `Bearer ${await getToken()}`,
...(body ? { "Content-Type": "application/json; charset=utf-8" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
} catch (err) {
if (attempt >= maxAttempts) throw err;
await sleep(2 ** attempt * 1000 + Math.random() * 250);
continue;
}
const text = await resp.text();
if (resp.ok) return text ? JSON.parse(text) : null;
if (resp.status === 401 && !refreshed) { refreshed = true; await getToken(true); attempt--; continue; }
if (!transient(resp.status) || attempt >= maxAttempts) throw new Error(`${method} ${path}: HTTP ${resp.status} ${text}`);
await sleep(2 ** attempt * 1000 + Math.random() * 250);
}
}
async function* paged(url, pageSize) {
for (let page = 1, pages = 1; page <= pages; page++) {
const rows = await request("GET", url(page));
if (!Array.isArray(rows) || rows.length === 0) return;
if (page === 1 && rows[0].TotalRowsCount != null) pages = Math.ceil(rows[0].TotalRowsCount / pageSize);
yield* rows;
}
}
/** GET /api/resources/{resource} pagina per pagina, con RecordSelection facoltativo. */
export function readAll(resource, recordSelection, pageSize = 500) {
const rs = recordSelection ? `&RecordSelection=${encodeURIComponent(recordSelection)}` : "";
return paged(p => `/api/resources/${resource}?PageSize=${pageSize}&PageNumber=${p}${rs}`, pageSize);
}
/** GET /api/sync/{resource}?LastUtcRun=… : solo i record variati dopo `since` (Date). */
export function readChanges(resource, since, pageSize = 500) {
const last = encodeURIComponent(since.toISOString().replace("T", " ").replace("Z", ""));
return paged(p => `/api/sync/${resource}?LastUtcRun=${last}&PageSize=${pageSize}&PageNumber=${p}`, pageSize);
}
/** POST /api/resources/{resource}/query (sola lettura). */
export function query(resource, filters, sort = [], pageSize = 100, pageNumber = 1) {
return request("POST", `/api/resources/${resource}/query?PageSize=${pageSize}&PageNumber=${pageNumber}`,
[{ ResourcesViewsFilters: filters, ResourcesViewsSortFields: sort }]);
}
/** POST /api/sync/{resource} a blocchi. SCRITTURA: usare solo su un tenant concordato. */
export async function sync(resource, records, batchSize = 500) {
for (let i = 0; i < records.length; i += batchSize) {
await request("POST", `/api/sync/${resource}`, records.slice(i, i + batchSize), 3);
}
}
Dal browser
Le chiamate verso https://api.e-cube.it sono accettate anche cross-origin. Non inserire però credenziali di servizio in codice eseguito nel browser: per le applicazioni web, fai passare le chiamate da un backend che custodisce le credenziali.