hrana
Version:
A tiny (1034b) runtime-agnostic Hrana HTTP client
216 lines (215 loc) • 4.75 kB
JavaScript
async function pipeline(c, input) {
let method = "POST";
let headers = new Headers();
let body = null;
if (c.token) {
headers.set("Authorization", `Bearer ${c.token}`);
}
if (input != null) {
body = JSON.stringify(input);
headers.set("Content-Type", "application/json");
}
let r = await request(c, "/v3/pipeline", {
method,
body,
headers
});
if (!r.ok) throw r;
let reply = (await r.json()).results[0];
if (reply.type === "ok") return reply.response.result;
let err = new Error(reply.error.message);
err.code = reply.error.code;
throw err;
}
function request(c, path, init) {
if (c.token) {
init ||= {};
init.headers = new Headers(init.headers);
init.headers.set("Authorization", `Bearer ${c.token}`);
}
return (c.fetch || fetch)(c.url + path, init);
}
/**
* Execute a single statement.
*
* > [!NOTE]
* > Throws an `Error` for pipeline statement errors.
* > Throws the `Response` for non-2xx status codes (eg, Authorization issues).
*/
export function execute(config, query) {
return pipeline(config, {
baton: null,
requests: [{
type: "execute",
stmt: query
}, { type: "close" }]
});
}
/**
* Execute a batch of statements, which will be executed sequentially.
*
* If the condition of a step is present and evaluates to false, the statement is not executed.
*
* > [!NOTE]
* > Throws an `Error` for pipeline statement errors.
* > Throws the `Response` for non-2xx status codes (eg, Authorization issues).
*/
export function batch(config, steps) {
return pipeline(config, {
baton: null,
requests: [{
type: "batch",
batch: { steps }
}, { type: "close" }]
});
}
/**
* Execute a transaction.
*
* > [!NOTE]
* > Throws an `Error` for pipeline statement errors.
* > Throws the `Response` for non-2xx status codes (eg, Authorization issues).
*
* @param config The Hrana configuration.
* @param type The transaction mode.
* @param stmts The statements to execute.
*/
export async function transaction(config, type, ...stmts) {
let i = 0, len = stmts.length;
let steps = [{ stmt: { sql: `BEGIN ${type}` } }];
for (; i < len; i++) {
steps.push({
stmt: stmts[i],
condition: {
type: "and",
conds: [{
type: "ok",
step: i
}, {
type: "not",
cond: { type: "is_autocommit" }
}]
}
});
}
len = steps.length;
steps.push({ stmt: { sql: "COMMIT" } });
steps.push({
stmt: { sql: "ROLLBACK" },
condition: {
type: "not",
cond: {
type: "ok",
step: len
}
}
});
let r = await batch(config, steps);
r.step_results = r.step_results.slice(1, len);
r.step_errors = r.step_errors.slice(1, len);
return r;
}
/**
* Check if the server supports Hrana V3 over HTTP with JSON encoding.
* @see https://github.com/tursodatabase/libsql/blob/main/docs/HRANA_3_SPEC.md#check-support-for-version-3-json
*/
export function supports(config) {
return request(config, "/v3").then((r) => r.ok);
}
/**
* Parse all Rows from the statement results.
*
* @param result The statement results.
* @param options The parsing options.
*/
export function parse(result, options) {
let { cols, rows } = result;
let i = 0, len = rows.length;
let k = 0, klen = cols.length;
let row, tmp;
let c, v;
let output = Array(len);
let mode;
let tx = {};
if (typeof options === "string") {
mode = options;
} else if (options) {
tx = options;
}
for (; i < len; i++) {
row = {};
tmp = rows[i];
for (k = 0; k < klen; k++) {
c = cols[k];
if (c.name) {
v = tmp[k];
row[c.name] = decode(v, mode);
if (tx[c.name]) {
row[c.name] = tx[c.name](row[c.name]);
}
}
}
output[i] = row;
}
return output;
}
export function decode(raw, mode) {
switch (raw.type) {
case "null": return null;
case "text": return raw.value;
case "float": return +raw.value;
case "integer": {
if (!mode || mode === "number") {
return +raw.value;
}
if (mode === "bigint") {
return BigInt(raw.value);
}
return raw.value;
}
}
raw;
let bin = atob(raw.base64);
let i = 0, size = bin.length;
let bytes = new Uint8Array(size);
for (; i < size; i++) {
bytes[i] = bin.charCodeAt(i);
}
return bytes;
}
/**
* Encode a JS value into its Hrana representation.
*
* @param v The value to encode.
*/
export function encode(v) {
if (v == null) {
return { type: "null" };
}
switch (typeof v) {
case "string": return {
type: "text",
value: v
};
case "number": return {
type: "float",
value: v
};
case "bigint": return {
type: "integer",
value: "" + v
};
case "boolean": return {
type: "integer",
value: v ? "1" : "0"
};
}
v;
return {
type: "blob",
base64: btoa(
// @ts-expect-error; Uint8Array is ArrayLike<number>
String.fromCharCode.apply(null, new Uint8Array(v))
)
};
}