Files
ResolveSpec/resolvespec-js/dist/index.js
T
warkanum b587cbd3c4 feat(headers): add support for custom HTTP headers in clients
* Introduced `headers` property in `ClientConfig` interface.
* Updated `HeaderSpecClient` and `ResolveSpecClient` to utilize custom headers.
* Implemented `mergeHeaders` function to handle case-insensitive header merging.
* Added tests for custom header functionality in clients.
2026-09-23 19:47:05 +02:00

485 lines
15 KiB
JavaScript

import { v4 as l } from "uuid";
function u(...r) {
const e = {};
for (const t of r)
for (const [s, n] of Object.entries(t)) {
for (const i of Object.keys(e))
i.toLowerCase() === s.toLowerCase() && delete e[i];
Object.defineProperty(e, s, { value: n, enumerable: !0, configurable: !0, writable: !0 });
}
return e;
}
function m(r) {
return u(
{ "Content-Type": "application/json" },
r.headers ?? {},
r.token ? { Authorization: `Bearer ${r.token}` } : {}
);
}
function p(r) {
const e = Object.entries(m(r)).map(([t, s]) => [t.toLowerCase(), s]).sort(([t], [s]) => t.localeCompare(s));
return JSON.stringify([r.baseUrl, e]);
}
const f = /* @__PURE__ */ new Map();
function v(r) {
const e = p(r);
let t = f.get(e);
return t || (t = new y(r), f.set(e, t)), t;
}
class y {
constructor(e) {
this.config = { ...e, headers: { ...e.headers } };
}
buildUrl(e, t, s) {
let n = `${this.config.baseUrl}/${e}/${t}`;
return s && (n += `/${s}`), n;
}
baseHeaders() {
return m(this.config);
}
async fetchWithError(e, t) {
const s = await fetch(e, t), n = await s.json();
if (!s.ok)
throw new Error(n.error?.message || "An error occurred");
return n;
}
async getMetadata(e, t) {
const s = this.buildUrl(e, t);
return this.fetchWithError(s, {
method: "GET",
headers: this.baseHeaders()
});
}
async read(e, t, s, n) {
const i = typeof s == "number" || typeof s == "string" ? String(s) : void 0, a = this.buildUrl(e, t, i), c = {
operation: "read",
id: Array.isArray(s) ? s : void 0,
options: n
};
return this.fetchWithError(a, {
method: "POST",
headers: this.baseHeaders(),
body: JSON.stringify(c)
});
}
async create(e, t, s, n) {
const i = this.buildUrl(e, t), a = {
operation: "create",
data: s,
options: n
};
return this.fetchWithError(i, {
method: "POST",
headers: this.baseHeaders(),
body: JSON.stringify(a)
});
}
async update(e, t, s, n, i) {
const a = typeof n == "number" || typeof n == "string" ? String(n) : void 0, c = this.buildUrl(e, t, a), o = {
operation: "update",
id: Array.isArray(n) ? n : void 0,
data: s,
options: i
};
return this.fetchWithError(c, {
method: "POST",
headers: this.baseHeaders(),
body: JSON.stringify(o)
});
}
async delete(e, t, s) {
const n = this.buildUrl(e, t, String(s)), i = {
operation: "delete"
};
return this.fetchWithError(n, {
method: "POST",
headers: this.baseHeaders(),
body: JSON.stringify(i)
});
}
}
const b = /* @__PURE__ */ new Map();
function O(r) {
const e = r.url;
let t = b.get(e);
return t || (t = new S(r), b.set(e, t)), t;
}
class S {
constructor(e) {
this.ws = null, this.messageHandlers = /* @__PURE__ */ new Map(), this.subscriptions = /* @__PURE__ */ new Map(), this.eventListeners = {}, this.state = "disconnected", this.reconnectAttempts = 0, this.reconnectTimer = null, this.heartbeatTimer = null, this.isManualClose = !1, this.config = {
url: e.url,
reconnect: e.reconnect ?? !0,
reconnectInterval: e.reconnectInterval ?? 3e3,
maxReconnectAttempts: e.maxReconnectAttempts ?? 10,
heartbeatInterval: e.heartbeatInterval ?? 3e4,
debug: e.debug ?? !1
};
}
async connect() {
if (this.ws?.readyState === WebSocket.OPEN) {
this.log("Already connected");
return;
}
return this.isManualClose = !1, this.setState("connecting"), new Promise((e, t) => {
try {
this.ws = new WebSocket(this.config.url), this.ws.onopen = () => {
this.log("Connected to WebSocket server"), this.setState("connected"), this.reconnectAttempts = 0, this.startHeartbeat(), this.emit("connect"), e();
}, this.ws.onmessage = (s) => {
this.handleMessage(s.data);
}, this.ws.onerror = (s) => {
this.log("WebSocket error:", s);
const n = new Error("WebSocket connection error");
this.emit("error", n), t(n);
}, this.ws.onclose = (s) => {
this.log("WebSocket closed:", s.code, s.reason), this.stopHeartbeat(), this.setState("disconnected"), this.emit("disconnect", s), this.config.reconnect && !this.isManualClose && this.reconnectAttempts < this.config.maxReconnectAttempts && (this.reconnectAttempts++, this.log(`Reconnection attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts}`), this.setState("reconnecting"), this.reconnectTimer = setTimeout(() => {
this.connect().catch((n) => {
this.log("Reconnection failed:", n);
});
}, this.config.reconnectInterval));
};
} catch (s) {
t(s);
}
});
}
disconnect() {
this.isManualClose = !0, this.reconnectTimer && (clearTimeout(this.reconnectTimer), this.reconnectTimer = null), this.stopHeartbeat(), this.ws && (this.setState("disconnecting"), this.ws.close(), this.ws = null), this.setState("disconnected"), this.messageHandlers.clear();
}
async request(e, t, s) {
this.ensureConnected();
const n = l(), i = {
id: n,
type: "request",
operation: e,
entity: t,
schema: s?.schema,
record_id: s?.record_id,
data: s?.data,
options: s?.options
};
return new Promise((a, c) => {
this.messageHandlers.set(n, (o) => {
o.success ? a(o.data) : c(new Error(o.error?.message || "Request failed"));
}), this.send(i), setTimeout(() => {
this.messageHandlers.has(n) && (this.messageHandlers.delete(n), c(new Error("Request timeout")));
}, 3e4);
});
}
async read(e, t) {
return this.request("read", e, {
schema: t?.schema,
record_id: t?.record_id,
options: {
filters: t?.filters,
columns: t?.columns,
sort: t?.sort,
preload: t?.preload,
limit: t?.limit,
offset: t?.offset
}
});
}
async create(e, t, s) {
return this.request("create", e, {
schema: s?.schema,
data: t
});
}
async update(e, t, s, n) {
return this.request("update", e, {
schema: n?.schema,
record_id: t,
data: s
});
}
async delete(e, t, s) {
await this.request("delete", e, {
schema: s?.schema,
record_id: t
});
}
async meta(e, t) {
return this.request("meta", e, {
schema: t?.schema
});
}
async subscribe(e, t, s) {
this.ensureConnected();
const n = l(), i = {
id: n,
type: "subscription",
operation: "subscribe",
entity: e,
schema: s?.schema,
options: {
filters: s?.filters
}
};
return new Promise((a, c) => {
this.messageHandlers.set(n, (o) => {
if (o.success && o.data?.subscription_id) {
const h = o.data.subscription_id;
this.subscriptions.set(h, {
id: h,
entity: e,
schema: s?.schema,
options: { filters: s?.filters },
callback: t
}), this.log(`Subscribed to ${e} with ID: ${h}`), a(h);
} else
c(new Error(o.error?.message || "Subscription failed"));
}), this.send(i), setTimeout(() => {
this.messageHandlers.has(n) && (this.messageHandlers.delete(n), c(new Error("Subscription timeout")));
}, 1e4);
});
}
async unsubscribe(e) {
this.ensureConnected();
const t = l(), s = {
id: t,
type: "subscription",
operation: "unsubscribe",
subscription_id: e
};
return new Promise((n, i) => {
this.messageHandlers.set(t, (a) => {
a.success ? (this.subscriptions.delete(e), this.log(`Unsubscribed from ${e}`), n()) : i(new Error(a.error?.message || "Unsubscribe failed"));
}), this.send(s), setTimeout(() => {
this.messageHandlers.has(t) && (this.messageHandlers.delete(t), i(new Error("Unsubscribe timeout")));
}, 1e4);
});
}
getSubscriptions() {
return Array.from(this.subscriptions.values());
}
getState() {
return this.state;
}
isConnected() {
return this.ws?.readyState === WebSocket.OPEN;
}
on(e, t) {
this.eventListeners[e] = t;
}
off(e) {
delete this.eventListeners[e];
}
// Private methods
handleMessage(e) {
try {
const t = JSON.parse(e);
switch (this.log("Received message:", t), this.emit("message", t), t.type) {
case "response":
this.handleResponse(t);
break;
case "notification":
this.handleNotification(t);
break;
case "pong":
break;
default:
this.log("Unknown message type:", t.type);
}
} catch (t) {
this.log("Error parsing message:", t);
}
}
handleResponse(e) {
const t = this.messageHandlers.get(e.id);
t && (t(e), this.messageHandlers.delete(e.id));
}
handleNotification(e) {
const t = this.subscriptions.get(e.subscription_id);
t?.callback && t.callback(e);
}
send(e) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
throw new Error("WebSocket is not connected");
const t = JSON.stringify(e);
this.log("Sending message:", e), this.ws.send(t);
}
startHeartbeat() {
this.heartbeatTimer || (this.heartbeatTimer = setInterval(() => {
if (this.isConnected()) {
const e = {
id: l(),
type: "ping"
};
this.send(e);
}
}, this.config.heartbeatInterval));
}
stopHeartbeat() {
this.heartbeatTimer && (clearInterval(this.heartbeatTimer), this.heartbeatTimer = null);
}
setState(e) {
this.state !== e && (this.state = e, this.emit("stateChange", e));
}
ensureConnected() {
if (!this.isConnected())
throw new Error("WebSocket is not connected. Call connect() first.");
}
emit(e, ...t) {
const s = this.eventListeners[e];
s && s(...t);
}
log(...e) {
this.config.debug && console.log("[WebSocketClient]", ...e);
}
}
function W(r) {
return typeof btoa == "function" ? "ZIP_" + btoa(r) : "ZIP_" + Buffer.from(r, "utf-8").toString("base64");
}
function H(r) {
let e = r;
return e.startsWith("ZIP_") ? (e = e.slice(4).replace(/[\n\r ]/g, ""), e = g(e)) : e.startsWith("__") && (e = e.slice(2).replace(/[\n\r ]/g, ""), e = g(e)), (e.startsWith("ZIP_") || e.startsWith("__")) && (e = H(e)), e;
}
function g(r) {
return typeof atob == "function" ? atob(r) : Buffer.from(r, "base64").toString("utf-8");
}
function d(r) {
const e = {};
if (r.columns?.length && (e["X-Select-Fields"] = r.columns.join(",")), r.omit_columns?.length && (e["X-Not-Select-Fields"] = r.omit_columns.join(",")), r.filters?.length)
for (const t of r.filters) {
const s = t.logic_operator ?? "AND", n = C(t.operator), i = E(t);
t.operator === "eq" && s === "AND" ? e[`X-FieldFilter-${t.column}`] = i : s === "OR" ? e[`X-SearchOr-${n}-${t.column}`] = i : e[`X-SearchOp-${n}-${t.column}`] = i;
}
if (r.sort?.length) {
const t = r.sort.map((s) => s.direction.toUpperCase() === "DESC" ? `-${s.column}` : `+${s.column}`);
e["X-Sort"] = t.join(",");
}
if (r.limit !== void 0 && (e["X-Limit"] = String(r.limit)), r.offset !== void 0 && (e["X-Offset"] = String(r.offset)), r.cursor_forward && (e["X-Cursor-Forward"] = r.cursor_forward), r.cursor_backward && (e["X-Cursor-Backward"] = r.cursor_backward), r.preload?.length) {
const t = r.preload.map((s) => s.columns?.length ? `${s.relation}:${s.columns.join(",")}` : s.relation);
e["X-Preload"] = t.join("|");
}
if (r.fetch_row_number && (e["X-Fetch-RowNumber"] = r.fetch_row_number), r.computedColumns?.length)
for (const t of r.computedColumns)
e[`X-CQL-SEL-${t.name}`] = t.expression;
if (r.customOperators?.length) {
const t = r.customOperators.map(
(s) => s.sql
);
e["X-Custom-SQL-W"] = t.join(" AND ");
}
return e;
}
function C(r) {
switch (r) {
case "eq":
return "equals";
case "neq":
return "notequals";
case "gt":
return "greaterthan";
case "gte":
return "greaterthanorequal";
case "lt":
return "lessthan";
case "lte":
return "lessthanorequal";
case "like":
case "ilike":
case "contains":
return "contains";
case "startswith":
return "beginswith";
case "endswith":
return "endswith";
case "in":
return "in";
case "between":
return "between";
case "between_inclusive":
return "betweeninclusive";
case "is_null":
return "empty";
case "is_not_null":
return "notempty";
default:
return r;
}
}
function E(r) {
return r.value === null || r.value === void 0 ? "" : Array.isArray(r.value) ? r.value.join(",") : String(r.value);
}
const w = /* @__PURE__ */ new Map();
function T(r) {
const e = p(r);
let t = w.get(e);
return t || (t = new _(r), w.set(e, t)), t;
}
class _ {
constructor(e) {
this.config = { ...e, headers: { ...e.headers } };
}
buildUrl(e, t, s) {
let n = `${this.config.baseUrl}/${e}/${t}`;
return s && (n += `/${s}`), n;
}
baseHeaders() {
return m(this.config);
}
async fetchWithError(e, t) {
const s = await fetch(e, t), n = await s.json();
if (!s.ok)
throw new Error(
n.error?.message || `${s.statusText} (${s.status})`
);
return {
data: n,
success: !0,
error: n.error ? n.error : void 0,
metadata: {
count: s.headers.get("content-range") ? Number(s.headers.get("content-range")?.split("/")[1]) : 0,
total: s.headers.get("content-range") ? Number(s.headers.get("content-range")?.split("/")[1]) : 0,
filtered: s.headers.get("content-range") ? Number(s.headers.get("content-range")?.split("/")[1]) : 0,
offset: s.headers.get("content-range") ? Number(
s.headers.get("content-range")?.split("/")[0].split("-")[0]
) : 0,
limit: s.headers.get("x-limit") ? Number(s.headers.get("x-limit")) : 0
}
};
}
async read(e, t, s, n) {
const i = this.buildUrl(e, t, s), a = n ? d(n) : {};
return this.fetchWithError(i, {
method: "GET",
headers: u(this.baseHeaders(), a)
});
}
async create(e, t, s, n) {
const i = this.buildUrl(e, t), a = n ? d(n) : {};
return this.fetchWithError(i, {
method: "POST",
headers: u(this.baseHeaders(), a),
body: JSON.stringify(s)
});
}
async update(e, t, s, n, i) {
const a = this.buildUrl(e, t, s), c = i ? d(i) : {};
return this.fetchWithError(a, {
method: "PUT",
headers: u(this.baseHeaders(), c),
body: JSON.stringify(n)
});
}
async delete(e, t, s) {
const n = this.buildUrl(e, t, s);
return this.fetchWithError(n, {
method: "DELETE",
headers: this.baseHeaders()
});
}
}
export {
_ as HeaderSpecClient,
y as ResolveSpecClient,
S as WebSocketClient,
d as buildHeaders,
H as decodeHeaderValue,
W as encodeHeaderValue,
T as getHeaderSpecClient,
v as getResolveSpecClient,
O as getWebSocketClient
};