Pular para o conteúdo principal

Node.js

Exemplo com fetch nativo (Node 18+), executando no PostgreSQL com pg ou no MySQL com mysql2. A API também aceita schemas Firebird 2.5+; para executar nesse banco, use node-firebird.

npm install pg # PostgreSQL
npm install mysql2 # MySQL
npm install node-firebird # Firebird

Cliente​

datafala.js
import { createHmac } from "node:crypto";

const DATAFALA_URL = process.env.DATAFALA_URL ?? "https://app.datafala.com.br";
const API_KEY = process.env.DATAFALA_API_KEY;
const END_USER_SECRET = process.env.DATAFALA_END_USER_SECRET;

export class DatafalaError extends Error {
constructor(status, body, headers) {
super(body.message ?? body.code ?? `HTTP ${status}`);
this.status = status;
this.code = body.code ?? "unknown";
this.details = body.details ?? {};
this.retryAfter = Number(headers.get("Retry-After")) || null;
this.requestId = headers.get("X-Request-ID");
}
}

export function endUserId(userId) {
return createHmac("sha256", END_USER_SECRET).update(String(userId)).digest("hex");
}

async function call(path, init = {}) {
const response = await fetch(`${DATAFALA_URL}${path}`, {
...init,
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
...init.headers,
},
signal: AbortSignal.timeout(60_000),
});
if (response.status === 204) return null;
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new DatafalaError(response.status, body, response.headers);
return body;
}

export function ask(question, userId, { maxRows } = {}) {
const payload = { question, end_user: endUserId(userId) };
if (maxRows) payload.max_rows = maxRows;
return call("/v1/ask", { method: "POST", body: JSON.stringify(payload) });
}

export function feedback(requestId, rating, comment) {
return call("/v1/feedback", {
method: "POST",
body: JSON.stringify({ request_id: requestId, rating, ...(comment ? { comment } : {}) }),
});
}

Converter os placeholders​

Os drivers de Node não entendem %(nome)s. Converta para o formato do driver e troque %% por %, numa única passada:

placeholders.js
// %(name)s -> $1, $2... (pg) ou ? (mysql2/node-firebird); %% -> %
export function toPositional(sql, params, style = "pg") {
const values = [];
const indexes = new Map();
const text = sql.replace(/%%|%\(([A-Za-z_][A-Za-z0-9_]*)\)s/g, (match, name) => {
if (match === "%%") return "%";
if (!(name in params)) throw new Error(`missing param ${name}`);
if (style === "pg") {
if (!indexes.has(name)) {
values.push(params[name]);
indexes.set(name, values.length);
}
return `$${indexes.get(name)}`;
}
values.push(params[name]);
return "?";
});
return { text, values };
}

Executar no PostgreSQL (pg)​

import pg from "pg";
import { ask } from "./datafala.js";
import { toPositional } from "./placeholders.js";

const readonly = new pg.Pool({
connectionString: process.env.READONLY_DATABASE_URL,
statement_timeout: 5000,
options: "-c default_transaction_read_only=on",
});

const answer = await ask("Faturamento por mês em 2026", 42);
const { text, values } = toPositional(answer.sql, answer.params, "pg");
const { rows } = await readonly.query(text, values);
console.log(answer.explanation, rows);

Executar no MySQL (mysql2)​

import mysql from "mysql2/promise";
import { toPositional } from "./placeholders.js";

const readonly = await mysql.createConnection({
uri: process.env.READONLY_MYSQL_URL,
});
await readonly.query("SET SESSION max_execution_time = 5000");

const { text, values } = toPositional(answer.sql, answer.params, "mysql");
const [rows] = await readonly.execute(text, values);

Express​

app.post("/api/assistant/ask", requireLogin, async (req, res) => {
try {
const answer = await ask(String(req.body.question ?? ""), req.user.id);
const { text, values } = toPositional(answer.sql, answer.params, "pg");
const { rows, fields } = await readonly.query(text, values);
res.json({
requestId: answer.request_id,
explanation: answer.explanation,
columns: fields.map((field) => field.name),
rows,
});
} catch (error) {
if (error instanceof DatafalaError && ["no_answer", "sql_rejected"].includes(error.code)) {
return res.status(422).json({ code: error.code, message: error.message });
}
res.status(503).json({ code: "assistant_unavailable" });
}
});