@onyx.dev/onyx-database
Version:
TypeScript client SDK for Onyx Database
1,590 lines (1,576 loc) • 52 kB
JavaScript
'use strict';
// src/config/defaults.ts
var DEFAULT_BASE_URL = "https://api.onyx.dev";
var sanitizeBaseUrl = (u) => u.replace(/\/+$/, "");
// src/errors/config-error.ts
var OnyxConfigError = class extends Error {
name = "OnyxConfigError";
constructor(message) {
super(message);
}
};
// src/config/chain.ts
var gProcess = globalThis.process;
var isNode = !!gProcess?.versions?.node;
var dbg = (...args) => {
if (gProcess?.env?.ONYX_DEBUG == "true") {
const fmt = (v) => {
if (typeof v === "string") return v;
try {
return JSON.stringify(v);
} catch {
return String(v);
}
};
gProcess.stderr?.write?.(`[onyx-config] ${args.map(fmt).join(" ")}
`);
}
};
function dropUndefined(obj) {
if (!obj) return {};
const out = {};
for (const [k, v] of Object.entries(obj)) {
if (v !== void 0) out[k] = v;
}
return out;
}
async function nodeImport(spec) {
return import(
/* @vite-ignore */
spec
);
}
function readEnv(targetId) {
if (!gProcess?.env) return {};
const env = gProcess.env;
const pick = (...keys) => {
for (const k of keys) {
const v = env[k];
if (typeof v === "string") {
const cleaned = v.replace(/[\r\n]+/g, "").trim();
if (cleaned !== "") return cleaned;
}
}
return void 0;
};
const envId = pick("ONYX_DATABASE_ID");
if (targetId && envId !== targetId) return {};
const res = dropUndefined({
baseUrl: pick("ONYX_DATABASE_BASE_URL"),
databaseId: envId,
apiKey: pick("ONYX_DATABASE_API_KEY"),
apiSecret: pick("ONYX_DATABASE_API_SECRET")
});
if (Object.keys(res).length === 0) return {};
dbg("env:", mask(res));
return res;
}
async function readProjectFile(databaseId) {
if (!isNode) return {};
const fs = await nodeImport("node:fs/promises");
const path = await nodeImport("node:path");
const cwd = gProcess?.cwd?.() ?? ".";
const tryRead = async (p) => {
const txt = await fs.readFile(p, "utf8");
const sanitized = txt.replace(/[\r\n]+/g, "");
const json = dropUndefined(JSON.parse(sanitized));
dbg("project file:", p, "\u2192", mask(json));
return json;
};
if (databaseId) {
const specific = path.resolve(cwd, `onyx-database-${databaseId}.json`);
try {
return await tryRead(specific);
} catch {
dbg("project file not found:", specific);
}
}
const fallback = path.resolve(cwd, "onyx-database.json");
try {
return await tryRead(fallback);
} catch {
dbg("project file not found:", fallback);
return {};
}
}
async function readHomeProfile(databaseId) {
if (!isNode) return {};
const fs = await nodeImport("node:fs/promises");
const os = await nodeImport("node:os");
const path = await nodeImport("node:path");
const home = os.homedir();
const dir = path.join(home, ".onyx");
const fileExists = async (p) => {
try {
await fs.access(p);
return true;
} catch {
return false;
}
};
const readProfile = async (p) => {
try {
const txt = await fs.readFile(p, "utf8");
const sanitized = txt.replace(/[\r\n]+/g, "");
const json = dropUndefined(JSON.parse(sanitized));
dbg("home profile used:", p, "\u2192", mask(json));
return json;
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
throw new OnyxConfigError(`Failed to read ${p}: ${msg}`);
}
};
if (databaseId) {
const specific = `${dir}/onyx-database-${databaseId}.json`;
if (await fileExists(specific)) return readProfile(specific);
dbg("no specific profile:", specific);
}
const defaultInDir = `${dir}/onyx-database.json`;
if (await fileExists(defaultInDir)) return readProfile(defaultInDir);
dbg("no default profile in dir:", defaultInDir);
const defaultInHome = `${home}/onyx-database.json`;
if (await fileExists(defaultInHome)) return readProfile(defaultInHome);
dbg("no home-root fallback:", defaultInHome);
if (!await fileExists(dir)) {
dbg("~/.onyx does not exist:", dir);
return {};
}
const files = await fs.readdir(dir).catch(() => []);
const matches2 = files.filter((f) => f.startsWith("onyx-database-") && f.endsWith(".json"));
if (matches2.length === 1) {
const only = `${dir}/${matches2[0]}`;
return readProfile(only);
}
if (matches2.length > 1) {
throw new OnyxConfigError(
"Multiple ~/.onyx/onyx-database-*.json profiles found. Specify databaseId via env or provide ./onyx-database.json."
);
}
dbg("no usable home profiles found in", dir);
return {};
}
async function readConfigPath(p) {
if (!isNode) return {};
const fs = await nodeImport("node:fs/promises");
const path = await nodeImport("node:path");
const cwd = gProcess?.cwd?.() ?? ".";
const resolved = path.isAbsolute(p) ? p : path.resolve(cwd, p);
try {
const txt = await fs.readFile(resolved, "utf8");
const sanitized = txt.replace(/[\r\n]+/g, "");
const json = dropUndefined(JSON.parse(sanitized));
dbg("config path:", resolved, "\u2192", mask(json));
return json;
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
throw new OnyxConfigError(`Failed to read ${resolved}: ${msg}`);
}
}
async function resolveConfig(input) {
const configPath = gProcess?.env?.ONYX_CONFIG_PATH;
const env = readEnv(input?.databaseId);
let cfgPath = {};
if (configPath) {
cfgPath = await readConfigPath(configPath);
}
const targetId = input?.databaseId ?? env.databaseId ?? cfgPath.databaseId;
let haveDbId = !!(input?.databaseId ?? env.databaseId ?? cfgPath.databaseId);
let haveApiKey = !!(input?.apiKey ?? env.apiKey ?? cfgPath.apiKey);
let haveApiSecret = !!(input?.apiSecret ?? env.apiSecret ?? cfgPath.apiSecret);
let project = {};
if (!(haveDbId && haveApiKey && haveApiSecret)) {
project = await readProjectFile(targetId);
if (project.databaseId) haveDbId = true;
if (project.apiKey) haveApiKey = true;
if (project.apiSecret) haveApiSecret = true;
}
let home = {};
if (!(haveDbId && haveApiKey && haveApiSecret)) {
home = await readHomeProfile(targetId);
}
const merged = {
baseUrl: DEFAULT_BASE_URL,
...dropUndefined(home),
...dropUndefined(project),
...dropUndefined(cfgPath),
...dropUndefined(env),
...dropUndefined(input)
};
dbg("merged (pre-validate):", mask(merged));
const baseUrl = sanitizeBaseUrl(merged.baseUrl ?? DEFAULT_BASE_URL);
const databaseId = merged.databaseId ?? "";
const apiKey = merged.apiKey ?? "";
const apiSecret = merged.apiSecret ?? "";
const gfetch = globalThis.fetch;
const fetchImpl = merged.fetch ?? (typeof gfetch === "function" ? (u, i) => gfetch(u, i) : async () => {
throw new OnyxConfigError("No fetch available; provide OnyxConfig.fetch");
});
const missing = [];
if (!databaseId) missing.push("databaseId");
if (!apiKey) missing.push("apiKey");
if (!apiSecret) missing.push("apiSecret");
if (missing.length) {
dbg("validation failed. merged:", mask(merged));
const sources = [
"env",
configPath ?? "env ONYX_CONFIG_PATH",
...isNode ? [
"./onyx-database-<databaseId>.json",
"./onyx-database.json",
"~/.onyx/onyx-database-<databaseId>.json",
"~/.onyx/onyx-database.json",
"~/onyx-database.json"
] : [],
"explicit config"
];
throw new OnyxConfigError(
`Missing required config: ${missing.join(", ")}. Sources: ${sources.join(", ")}`
);
}
const resolved = { baseUrl, databaseId, apiKey, apiSecret, fetch: fetchImpl };
const source = {
databaseId: input?.databaseId ? "explicit config" : env.databaseId ? "env" : cfgPath.databaseId ? "env ONYX_CONFIG_PATH" : project.databaseId ? "project file" : home.databaseId ? "home profile" : "unknown",
apiKey: input?.apiKey ? "explicit config" : env.apiKey ? "env" : cfgPath.apiKey ? "env ONYX_CONFIG_PATH" : project.apiKey ? "project file" : home.apiKey ? "home profile" : "unknown",
apiSecret: input?.apiSecret ? "explicit config" : env.apiSecret ? "env" : cfgPath.apiSecret ? "env ONYX_CONFIG_PATH" : project.apiSecret ? "project file" : home.apiSecret ? "home profile" : "unknown"
};
dbg("credential source:", JSON.stringify(source));
dbg("resolved:", mask(resolved));
return resolved;
}
function mask(obj) {
if (!obj) return obj;
const clone = { ...obj };
if (typeof clone.apiKey === "string") clone.apiKey = "***";
if (typeof clone.apiSecret === "string") clone.apiSecret = "***";
return clone;
}
// src/errors/http-error.ts
var OnyxHttpError = class extends Error {
name = "OnyxHttpError";
status;
statusText;
body;
rawBody;
constructor(message, status, statusText, body, rawBody) {
super(message);
this.status = status;
this.statusText = statusText;
this.body = body;
this.rawBody = rawBody;
}
toJSON() {
return {
name: this.name,
message: this.message,
status: this.status,
statusText: this.statusText,
body: this.body,
rawBody: this.rawBody,
stack: this.stack
};
}
[Symbol.for("nodejs.util.inspect.custom")]() {
return this.toJSON();
}
};
// src/core/http.ts
function parseJsonAllowNaN(txt) {
try {
return JSON.parse(txt);
} catch {
const fixed = txt.replace(/(:\s*)(NaN|Infinity|-Infinity)(\s*[,}])/g, "$1null$3");
return JSON.parse(fixed);
}
}
var HttpClient = class {
baseUrl;
apiKey;
apiSecret;
fetchImpl;
defaults;
requestLoggingEnabled;
responseLoggingEnabled;
constructor(opts) {
if (!opts.baseUrl || opts.baseUrl.trim() === "") {
throw new OnyxConfigError("baseUrl is required");
}
try {
new URL(opts.baseUrl);
} catch {
throw new OnyxConfigError("baseUrl must include protocol, e.g. https://");
}
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
this.apiKey = opts.apiKey;
this.apiSecret = opts.apiSecret;
const gfetch = globalThis.fetch;
if (opts.fetchImpl) {
this.fetchImpl = opts.fetchImpl;
} else if (typeof gfetch === "function") {
this.fetchImpl = (url, init) => gfetch(url, init);
} else {
throw new Error("global fetch is not available; provide OnyxConfig.fetch");
}
this.defaults = Object.assign({}, opts.defaultHeaders);
const envDebug = globalThis.process?.env?.ONYX_DEBUG === "true";
this.requestLoggingEnabled = !!opts.requestLoggingEnabled || envDebug;
this.responseLoggingEnabled = !!opts.responseLoggingEnabled || envDebug;
}
headers(extra) {
const extras = { ...extra ?? {} };
delete extras["x-onyx-key"];
delete extras["x-onyx-secret"];
return {
"x-onyx-key": this.apiKey,
"x-onyx-secret": this.apiSecret,
"Accept": "application/json",
"Content-Type": "application/json",
...this.defaults,
...extras
};
}
async request(method, path, body, extraHeaders) {
if (!path.startsWith("/")) {
throw new OnyxConfigError("path must start with /");
}
const url = `${this.baseUrl}${path}`;
const headers = this.headers({
...method === "DELETE" ? { Prefer: "return=representation" } : {},
...extraHeaders ?? {}
});
if (body == null) delete headers["Content-Type"];
if (this.requestLoggingEnabled) {
console.log(`${method} ${url}`);
if (body != null) {
const logBody = typeof body === "string" ? body : JSON.stringify(body);
console.log(logBody);
}
const headerLog = { ...headers, "x-onyx-secret": "[REDACTED]" };
console.log("Headers:", headerLog);
}
const payload = body == null ? void 0 : typeof body === "string" ? body : JSON.stringify(body);
const init = {
method,
headers,
body: payload
};
const isQuery = path.includes("/query/") && !/\/query\/(?:update|delete)\//.test(path);
const canRetry = method === "GET" || isQuery;
const maxAttempts = canRetry ? 3 : 1;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const res = await this.fetchImpl(url, init);
const contentType = res.headers.get("Content-Type") || "";
const raw = await res.text();
if (this.responseLoggingEnabled) {
const statusLine = `${res.status} ${res.statusText}`.trim();
console.log(statusLine);
if (raw.trim().length > 0) {
console.log(raw);
}
}
const isJson = raw.trim().length > 0 && (contentType.includes("application/json") || /^[\[{]/.test(raw.trim()));
const data = isJson ? parseJsonAllowNaN(raw) : raw;
if (!res.ok) {
const msg = typeof data === "object" && data !== null && "error" in data && typeof data.error?.message === "string" ? String(data.error.message) : `${res.status} ${res.statusText}`;
if (canRetry && res.status >= 500 && attempt + 1 < maxAttempts) {
await new Promise((r) => setTimeout(r, 100 * 2 ** attempt));
continue;
}
throw new OnyxHttpError(msg, res.status, res.statusText, data, raw);
}
return data;
} catch (err) {
const retryable = canRetry && (!(err instanceof OnyxHttpError) || err.status >= 500);
if (attempt + 1 < maxAttempts && retryable) {
await new Promise((r) => setTimeout(r, 100 * 2 ** attempt));
continue;
}
throw err;
}
}
throw new Error("Request failed after retries");
}
};
// src/core/stream.ts
var debug = (...args) => {
if (globalThis.process?.env?.ONYX_STREAM_DEBUG == "true")
console.log("[onyx-stream]", ...args);
};
async function openJsonLinesStream(fetchImpl, url, init = {}, handlers = {}) {
const decoder = new TextDecoder("utf-8");
let buffer = "";
let canceled = false;
let currentReader = null;
let retryCount = 0;
const maxRetries = 4;
const processLine = (line) => {
const trimmed = line.trim();
debug("line", trimmed);
if (!trimmed || trimmed.startsWith(":")) return;
const jsonLine = trimmed.startsWith("data:") ? trimmed.slice(5).trim() : trimmed;
let obj;
try {
obj = parseJsonAllowNaN(jsonLine);
} catch {
return;
}
const rawAction = obj.action ?? obj.event ?? obj.type ?? obj.eventType ?? obj.changeType;
const entity = obj.entity;
const action = rawAction?.toUpperCase();
if (action === "CREATE" || action === "CREATED" || action === "ADDED" || action === "ADD" || action === "INSERT" || action === "INSERTED")
handlers.onItemAdded?.(entity);
else if (action === "UPDATE" || action === "UPDATED")
handlers.onItemUpdated?.(entity);
else if (action === "DELETE" || action === "DELETED" || action === "REMOVE" || action === "REMOVED")
handlers.onItemDeleted?.(entity);
const canonical = action === "ADDED" || action === "ADD" || action === "CREATE" || action === "CREATED" || action === "INSERT" || action === "INSERTED" ? "CREATE" : action === "UPDATED" || action === "UPDATE" ? "UPDATE" : action === "DELETED" || action === "DELETE" || action === "REMOVE" || action === "REMOVED" ? "DELETE" : action;
if (canonical && canonical !== "KEEP_ALIVE")
handlers.onItem?.(entity ?? null, canonical);
debug("dispatch", canonical, entity);
};
const connect = async () => {
if (canceled) return;
debug("connecting", url);
try {
const res = await fetchImpl(url, {
method: init.method ?? "PUT",
headers: init.headers ?? {},
body: init.body
});
debug("response", res.status, res.statusText);
if (!res.ok) {
const raw = await res.text();
let parsed = raw;
try {
parsed = parseJsonAllowNaN(raw);
} catch {
}
debug("non-ok", res.status);
throw new OnyxHttpError(`${res.status} ${res.statusText}`, res.status, res.statusText, parsed, raw);
}
const body = res.body;
if (!body || typeof body.getReader !== "function") {
debug("no reader");
return;
}
currentReader = body.getReader();
debug("connected");
retryCount = 0;
pump();
} catch (err) {
debug("connect error", err);
if (canceled) return;
if (retryCount >= maxRetries) return;
const delay = Math.min(1e3 * 2 ** retryCount, 3e4);
retryCount++;
await new Promise((resolve) => setTimeout(resolve, delay));
void connect();
}
};
const pump = () => {
if (canceled || !currentReader) return;
currentReader.read().then(({ done, value }) => {
if (canceled) return;
debug("chunk", { done, length: value?.length ?? 0 });
if (done) {
debug("done");
void connect();
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) processLine(line);
pump();
}).catch((err) => {
debug("pump error", err);
if (!canceled) void connect();
});
};
await connect();
return {
cancel() {
if (canceled) return;
canceled = true;
try {
currentReader?.cancel();
} catch {
}
}
};
}
// src/builders/query-results.ts
var QueryResults = class extends Array {
/** Token for the next page of results or null. */
nextPage;
fetcher;
/**
* @param records - Records in the current page.
* @param nextPage - Token representing the next page.
* @param fetcher - Function used to fetch the next page when needed.
* @example
* ```ts
* const results = new QueryResults(users, token, t => fetchMore(t));
* ```
*/
constructor(records, nextPage, fetcher) {
const items = (() => {
if (records == null) return [];
if (Array.isArray(records)) return records;
if (typeof records[Symbol.iterator] === "function") {
return Array.from(records);
}
if (typeof records.length === "number") {
return Array.from(records);
}
return [records];
})();
super(...items);
Object.setPrototypeOf(this, new.target.prototype);
this.nextPage = nextPage;
this.fetcher = fetcher;
}
/**
* Returns the first record in the result set.
* @throws Error if the result set is empty.
* @example
* ```ts
* const user = results.first();
* ```
*/
first() {
if (this.length === 0) throw new Error("QueryResults is empty");
return this[0];
}
/**
* Returns the first record or `null` if the result set is empty.
* @example
* ```ts
* const user = results.firstOrNull();
* ```
*/
firstOrNull() {
return this.length > 0 ? this[0] : null;
}
/**
* Checks whether the current page has no records.
* @example
* ```ts
* if (results.isEmpty()) console.log('no data');
* ```
*/
isEmpty() {
return this.length === 0;
}
/**
* Number of records on the current page.
* @example
* ```ts
* console.log(results.size());
* ```
*/
size() {
return this.length;
}
/**
* Iterates over each record on the current page only.
* @param action - Function to invoke for each record.
* @param thisArg - Optional `this` binding for the callback.
* @example
* ```ts
* results.forEachOnPage(u => console.log(u.id));
* ```
*/
forEachOnPage(action, thisArg) {
super.forEach((value, index) => {
action.call(thisArg, value, index, this);
});
}
/**
* Iterates over every record across all pages sequentially.
* @param action - Function executed for each record. Returning `false`
* stops iteration early.
* @param thisArg - Optional `this` binding for the callback.
* @example
* ```ts
* await results.forEach(u => {
* console.log(u.id);
* });
* ```
*/
forEach(action, thisArg) {
let index = 0;
return this.forEachAll(async (item) => {
const result = await action.call(thisArg, item, index, this);
index += 1;
return result;
});
}
/**
* Iterates over every record across all pages sequentially.
* @param action - Function executed for each record. Returning `false`
* stops iteration early.
* @example
* ```ts
* await results.forEachAll(u => {
* if (u.disabled) return false;
* });
* ```
*/
async forEachAll(action) {
await this.forEachPage(async (records) => {
for (const r of records) {
const res = await action(r);
if (res === false) return false;
}
return true;
});
}
/**
* Iterates page by page across the result set.
* @param action - Function invoked with each page of records. Returning
* `false` stops iteration.
* @example
* ```ts
* await results.forEachPage(page => {
* console.log(page.length);
* });
* ```
*/
async forEachPage(action) {
let page = this;
while (page) {
const cont = await action(Array.from(page));
if (cont === false) return;
if (page.nextPage && page.fetcher) {
page = await page.fetcher(page.nextPage);
} else {
page = null;
}
}
}
/**
* Collects all records from every page into a single array.
* @returns All records.
* @example
* ```ts
* const allUsers = await results.getAllRecords();
* ```
*/
async getAllRecords() {
const all = [];
await this.forEachPage((records) => {
all.push(...records);
});
return all;
}
/**
* Filters all records using the provided predicate.
* @param predicate - Function used to test each record.
* @example
* ```ts
* const enabled = await results.filterAll(u => u.enabled);
* ```
*/
async filterAll(predicate) {
const all = await this.getAllRecords();
return all.filter(predicate);
}
/**
* Maps all records using the provided transform.
* @param transform - Mapping function.
* @example
* ```ts
* const names = await results.mapAll(u => u.name);
* ```
*/
async mapAll(transform) {
const all = await this.getAllRecords();
return all.map(transform);
}
/**
* Extracts values for a field across all records.
* @param field - Name of the field to pluck.
* @example
* ```ts
* const ids = await results.values('id');
* ```
*/
// @ts-expect-error overriding Array#values
async values(field) {
const all = await this.getAllRecords();
return all.map((r) => r[field]);
}
/**
* Maximum value produced by the selector across all records.
* @param selector - Function extracting a numeric value.
* @example
* ```ts
* const maxAge = await results.maxOfDouble(u => u.age);
* ```
*/
async maxOfDouble(selector) {
const all = await this.getAllRecords();
return all.reduce((max2, r) => Math.max(max2, selector(r)), -Infinity);
}
/**
* Minimum value produced by the selector across all records.
* @param selector - Function extracting a numeric value.
* @example
* ```ts
* const minAge = await results.minOfDouble(u => u.age);
* ```
*/
async minOfDouble(selector) {
const all = await this.getAllRecords();
return all.reduce((min2, r) => Math.min(min2, selector(r)), Infinity);
}
/**
* Sum of values produced by the selector across all records.
* @param selector - Function extracting a numeric value.
* @example
* ```ts
* const total = await results.sumOfDouble(u => u.score);
* ```
*/
async sumOfDouble(selector) {
const all = await this.getAllRecords();
return all.reduce((sum2, r) => sum2 + selector(r), 0);
}
/**
* Maximum float value from the selector.
* @param selector - Function extracting a numeric value.
*/
async maxOfFloat(selector) {
return this.maxOfDouble(selector);
}
/**
* Minimum float value from the selector.
* @param selector - Function extracting a numeric value.
*/
async minOfFloat(selector) {
return this.minOfDouble(selector);
}
/**
* Sum of float values from the selector.
* @param selector - Function extracting a numeric value.
*/
async sumOfFloat(selector) {
return this.sumOfDouble(selector);
}
/**
* Maximum integer value from the selector.
* @param selector - Function extracting a numeric value.
*/
async maxOfInt(selector) {
return this.maxOfDouble(selector);
}
/**
* Minimum integer value from the selector.
* @param selector - Function extracting a numeric value.
*/
async minOfInt(selector) {
return this.minOfDouble(selector);
}
/**
* Sum of integer values from the selector.
* @param selector - Function extracting a numeric value.
*/
async sumOfInt(selector) {
return this.sumOfDouble(selector);
}
/**
* Maximum long value from the selector.
* @param selector - Function extracting a numeric value.
*/
async maxOfLong(selector) {
return this.maxOfDouble(selector);
}
/**
* Minimum long value from the selector.
* @param selector - Function extracting a numeric value.
*/
async minOfLong(selector) {
return this.minOfDouble(selector);
}
/**
* Sum of long values from the selector.
* @param selector - Function extracting a numeric value.
*/
async sumOfLong(selector) {
return this.sumOfDouble(selector);
}
/**
* Sum of bigint values from the selector.
* @param selector - Function extracting a bigint value.
* @example
* ```ts
* const total = await results.sumOfBigInt(u => u.balance);
* ```
*/
async sumOfBigInt(selector) {
const all = await this.getAllRecords();
return all.reduce((sum2, r) => sum2 + selector(r), 0n);
}
/**
* Executes an action for each page in parallel.
* @param action - Function executed for each record concurrently.
* @example
* ```ts
* await results.forEachPageParallel(async u => sendEmail(u));
* ```
*/
async forEachPageParallel(action) {
await this.forEachPage((records) => Promise.all(records.map(action)).then(() => true));
}
};
// src/builders/cascade-relationship-builder.ts
var CascadeRelationshipBuilder = class {
graphName;
typeName;
target;
/**
* Set the graph name component.
*
* @param name Graph name or namespace.
* @example
* ```ts
* builder.graph('programs');
* ```
*/
graph(name) {
this.graphName = name;
return this;
}
/**
* Set the graph type component.
*
* @param type Graph type to target.
* @example
* ```ts
* builder.graphType('StreamingProgram');
* ```
*/
graphType(type) {
this.typeName = type;
return this;
}
/**
* Set the target field for the relationship.
*
* @param field Target field name.
* @example
* ```ts
* builder.targetField('channelId');
* ```
*/
targetField(field) {
this.target = field;
return this;
}
/**
* Produce the cascade relationship string using the provided source field.
*
* @param field Source field name.
* @example
* ```ts
* const rel = builder
* .graph('programs')
* .graphType('StreamingProgram')
* .targetField('channelId')
* .sourceField('id');
* // rel === 'programs:StreamingProgram(channelId, id)'
* ```
*/
sourceField(field) {
if (!this.graphName || !this.typeName || !this.target) {
throw new Error("Cascade relationship requires graph, type, target, and source fields");
}
return `${this.graphName}:${this.typeName}(${this.target}, ${field})`;
}
};
// src/errors/onyx-error.ts
var OnyxError = class extends Error {
name = "OnyxError";
constructor(message) {
super(message);
}
};
// src/impl/onyx.ts
var DEFAULT_CACHE_TTL = 5 * 60 * 1e3;
var cachedCfg = null;
function resolveConfigWithCache(config) {
const ttl = config?.ttl ?? DEFAULT_CACHE_TTL;
const now = Date.now();
if (cachedCfg && cachedCfg.expires > now) {
return cachedCfg.promise;
}
const { ttl: _ttl, requestLoggingEnabled: _reqLog, responseLoggingEnabled: _resLog, ...rest } = config ?? {};
const promise = resolveConfig(rest);
cachedCfg = { promise, expires: now + ttl };
return promise;
}
function clearCacheConfig() {
cachedCfg = null;
}
function toSingleCondition(criteria) {
return { conditionType: "SingleCondition", criteria };
}
function toCondition(input) {
if (typeof input.toCondition === "function") {
return input.toCondition();
}
const c2 = input;
if (c2 && typeof c2.field === "string" && typeof c2.operator === "string") {
return toSingleCondition(c2);
}
throw new Error("Invalid condition passed to builder.");
}
function serializeDates(value) {
if (value instanceof Date) return value.toISOString();
if (Array.isArray(value)) return value.map(serializeDates);
if (value && typeof value === "object") {
const out = {};
for (const [k, v] of Object.entries(value)) {
out[k] = serializeDates(v);
}
return out;
}
return value;
}
var OnyxDatabaseImpl = class {
cfgPromise;
resolved = null;
http = null;
streams = /* @__PURE__ */ new Set();
requestLoggingEnabled;
responseLoggingEnabled;
defaultPartition;
constructor(config) {
this.requestLoggingEnabled = !!config?.requestLoggingEnabled;
this.responseLoggingEnabled = !!config?.responseLoggingEnabled;
this.defaultPartition = config?.partition;
this.cfgPromise = resolveConfigWithCache(config);
}
async ensureClient() {
if (!this.resolved) {
this.resolved = await this.cfgPromise;
}
if (!this.http) {
this.http = new HttpClient({
baseUrl: this.resolved.baseUrl,
apiKey: this.resolved.apiKey,
apiSecret: this.resolved.apiSecret,
fetchImpl: this.resolved.fetch,
requestLoggingEnabled: this.requestLoggingEnabled,
responseLoggingEnabled: this.responseLoggingEnabled
});
}
return {
http: this.http,
fetchImpl: this.resolved.fetch,
baseUrl: this.resolved.baseUrl,
databaseId: this.resolved.databaseId
};
}
registerStream(handle) {
this.streams.add(handle);
return {
cancel: () => {
try {
handle.cancel();
} finally {
this.streams.delete(handle);
}
}
};
}
/** -------- IOnyxDatabase -------- */
from(table) {
return new QueryBuilderImpl(this, String(table), this.defaultPartition);
}
select(...fields) {
const qb = new QueryBuilderImpl(
this,
null,
this.defaultPartition
);
qb.selectFields(...fields);
return qb;
}
cascade(...relationships) {
const cb = new CascadeBuilderImpl(this);
return cb.cascade(...relationships);
}
cascadeBuilder() {
return new CascadeRelationshipBuilder();
}
// Impl
save(table, entityOrEntities, options) {
if (arguments.length === 1) {
return new SaveBuilderImpl(this, table);
}
return this._saveInternal(table, entityOrEntities, options);
}
async batchSave(table, entities, batchSize = 1e3, options) {
for (let i = 0; i < entities.length; i += batchSize) {
const chunk = entities.slice(i, i + batchSize);
if (chunk.length) {
await this._saveInternal(String(table), chunk, options);
}
}
}
async findById(table, primaryKey, options) {
const { http, databaseId } = await this.ensureClient();
const params = new URLSearchParams();
const partition = options?.partition ?? this.defaultPartition;
if (partition) params.append("partition", partition);
if (options?.resolvers?.length) params.append("resolvers", options.resolvers.join(","));
const path = `/data/${encodeURIComponent(databaseId)}/${encodeURIComponent(
String(table)
)}/${encodeURIComponent(primaryKey)}${params.toString() ? `?${params.toString()}` : ""}`;
try {
return await http.request("GET", path);
} catch (err) {
if (err instanceof OnyxHttpError && err.status === 404) return null;
throw err;
}
}
async delete(table, primaryKey, options) {
const { http, databaseId } = await this.ensureClient();
const params = new URLSearchParams();
const partition = options?.partition ?? this.defaultPartition;
if (partition) params.append("partition", partition);
if (options?.relationships?.length) {
params.append("relationships", options.relationships.map(encodeURIComponent).join(","));
}
const path = `/data/${encodeURIComponent(databaseId)}/${encodeURIComponent(
table
)}/${encodeURIComponent(primaryKey)}${params.toString() ? `?${params.toString()}` : ""}`;
return http.request("DELETE", path);
}
async saveDocument(doc) {
const { http, databaseId } = await this.ensureClient();
const path = `/data/${encodeURIComponent(databaseId)}/document`;
return http.request("PUT", path, serializeDates(doc));
}
async getDocument(documentId, options) {
const { http, databaseId } = await this.ensureClient();
const params = new URLSearchParams();
if (options?.width != null) params.append("width", String(options.width));
if (options?.height != null) params.append("height", String(options.height));
const path = `/data/${encodeURIComponent(databaseId)}/document/${encodeURIComponent(
documentId
)}${params.toString() ? `?${params.toString()}` : ""}`;
return http.request("GET", path);
}
async deleteDocument(documentId) {
const { http, databaseId } = await this.ensureClient();
const path = `/data/${encodeURIComponent(databaseId)}/document/${encodeURIComponent(
documentId
)}`;
return http.request("DELETE", path);
}
close() {
for (const h of Array.from(this.streams)) {
try {
h.cancel();
} catch {
} finally {
this.streams.delete(h);
}
}
}
/** -------- internal helpers used by builders -------- */
async _count(table, select, partition) {
const { http, databaseId } = await this.ensureClient();
const params = new URLSearchParams();
const p = partition ?? this.defaultPartition;
if (p) params.append("partition", p);
const path = `/data/${encodeURIComponent(databaseId)}/query/count/${encodeURIComponent(
table
)}${params.toString() ? `?${params.toString()}` : ""}`;
return http.request("PUT", path, serializeDates(select));
}
async _queryPage(table, select, opts = {}) {
const { http, databaseId } = await this.ensureClient();
const params = new URLSearchParams();
if (opts.pageSize != null) params.append("pageSize", String(opts.pageSize));
if (opts.nextPage) params.append("nextPage", opts.nextPage);
const p = opts.partition ?? this.defaultPartition;
if (p) params.append("partition", p);
const path = `/data/${encodeURIComponent(databaseId)}/query/${encodeURIComponent(
table
)}${params.toString() ? `?${params.toString()}` : ""}`;
return http.request("PUT", path, serializeDates(select));
}
async _update(table, update, partition) {
const { http, databaseId } = await this.ensureClient();
const params = new URLSearchParams();
const p = partition ?? this.defaultPartition;
if (p) params.append("partition", p);
const path = `/data/${encodeURIComponent(databaseId)}/query/update/${encodeURIComponent(
table
)}${params.toString() ? `?${params.toString()}` : ""}`;
return http.request("PUT", path, serializeDates(update));
}
async _deleteByQuery(table, select, partition) {
const { http, databaseId } = await this.ensureClient();
const params = new URLSearchParams();
const p = partition ?? this.defaultPartition;
if (p) params.append("partition", p);
const path = `/data/${encodeURIComponent(databaseId)}/query/delete/${encodeURIComponent(
table
)}${params.toString() ? `?${params.toString()}` : ""}`;
return http.request("PUT", path, serializeDates(select));
}
async _stream(table, select, includeQueryResults, keepAlive, handlers) {
const { http, baseUrl, databaseId, fetchImpl } = await this.ensureClient();
const params = new URLSearchParams();
if (includeQueryResults) params.append("includeQueryResults", "true");
if (keepAlive) params.append("keepAlive", "true");
const url = `${baseUrl}/data/${encodeURIComponent(databaseId)}/query/stream/${encodeURIComponent(
table
)}${params.toString() ? `?${params.toString()}` : ""}`;
const handle = await openJsonLinesStream(
fetchImpl,
url,
{
method: "PUT",
headers: http.headers({
Accept: "application/x-ndjson",
"Content-Type": "application/json"
}),
body: JSON.stringify(serializeDates(select))
},
handlers
);
return this.registerStream(handle);
}
async _saveInternal(table, entityOrEntities, options) {
const { http, databaseId } = await this.ensureClient();
const params = new URLSearchParams();
if (options?.relationships?.length) {
params.append("relationships", options.relationships.map(encodeURIComponent).join(","));
}
const path = `/data/${encodeURIComponent(databaseId)}/${encodeURIComponent(table)}${params.toString() ? `?${params.toString()}` : ""}`;
return http.request("PUT", path, serializeDates(entityOrEntities));
}
};
var QueryBuilderImpl = class {
db;
table;
fields = null;
resolvers = null;
conditions = null;
sort = null;
limitValue = null;
distinctValue = false;
groupByValues = null;
partitionValue;
pageSizeValue = null;
nextPageValue = null;
mode = "select";
updates = null;
onItemAddedListener = null;
onItemUpdatedListener = null;
onItemDeletedListener = null;
onItemListener = null;
constructor(db, table, partition) {
this.db = db;
this.table = table;
this.partitionValue = partition;
}
ensureTable() {
if (!this.table) throw new Error("Table is not defined. Call from(<table>) first.");
return this.table;
}
toSelectQuery() {
return {
type: "SelectQuery",
fields: this.fields,
conditions: this.conditions,
sort: this.sort,
limit: this.limitValue,
distinct: this.distinctValue,
groupBy: this.groupByValues,
partition: this.partitionValue ?? null,
resolvers: this.resolvers
};
}
from(table) {
this.table = table;
return this;
}
selectFields(...fields) {
const flat = fields.flatMap((f) => Array.isArray(f) ? f : [f]);
this.fields = flat.length > 0 ? flat : null;
return this;
}
resolve(...values) {
const flat = values.flatMap((v) => Array.isArray(v) ? v : [v]);
this.resolvers = flat.length > 0 ? flat : null;
return this;
}
where(condition) {
const c2 = toCondition(condition);
if (!this.conditions) {
this.conditions = c2;
} else {
this.conditions = {
conditionType: "CompoundCondition",
operator: "AND",
conditions: [this.conditions, c2]
};
}
return this;
}
and(condition) {
const c2 = toCondition(condition);
if (!this.conditions) {
this.conditions = c2;
} else if (this.conditions.conditionType === "CompoundCondition" && this.conditions.operator === "AND") {
this.conditions.conditions.push(c2);
} else {
this.conditions = {
conditionType: "CompoundCondition",
operator: "AND",
conditions: [this.conditions, c2]
};
}
return this;
}
or(condition) {
const c2 = toCondition(condition);
if (!this.conditions) {
this.conditions = c2;
} else if (this.conditions.conditionType === "CompoundCondition" && this.conditions.operator === "OR") {
this.conditions.conditions.push(c2);
} else {
this.conditions = {
conditionType: "CompoundCondition",
operator: "OR",
conditions: [this.conditions, c2]
};
}
return this;
}
orderBy(...sorts) {
this.sort = sorts;
return this;
}
groupBy(...fields) {
this.groupByValues = fields.length ? fields : null;
return this;
}
distinct() {
this.distinctValue = true;
return this;
}
limit(n) {
this.limitValue = n;
return this;
}
inPartition(partition) {
this.partitionValue = partition;
return this;
}
pageSize(n) {
this.pageSizeValue = n;
return this;
}
nextPage(token) {
this.nextPageValue = token;
return this;
}
setUpdates(updates) {
this.mode = "update";
this.updates = updates;
return this;
}
async count() {
if (this.mode !== "select") throw new Error("Cannot call count() in update mode.");
const table = this.ensureTable();
return this.db._count(table, this.toSelectQuery(), this.partitionValue);
}
async page(options = {}) {
if (this.mode !== "select") throw new Error("Cannot call page() in update mode.");
const table = this.ensureTable();
const final = {
pageSize: this.pageSizeValue ?? options.pageSize,
nextPage: this.nextPageValue ?? options.nextPage,
partition: this.partitionValue
};
return this.db._queryPage(table, this.toSelectQuery(), final);
}
list(options = {}) {
const size = this.pageSizeValue ?? options.pageSize;
const pgPromise = this.page(options).then((pg) => {
const fetcher = (token) => this.nextPage(token).list({ pageSize: size });
return new QueryResults(Array.isArray(pg.records) ? pg.records : [], pg.nextPage ?? null, fetcher);
});
for (const m of Object.getOwnPropertyNames(QueryResults.prototype)) {
if (m === "constructor") continue;
pgPromise[m] = (...args) => pgPromise.then((res) => res[m](...args));
}
return pgPromise;
}
async firstOrNull() {
if (this.mode !== "select") throw new Error("Cannot call firstOrNull() in update mode.");
if (!this.conditions) throw new OnyxError("firstOrNull() requires a where() clause.");
this.limitValue = 1;
const pg = await this.page();
return Array.isArray(pg.records) && pg.records.length > 0 ? pg.records[0] : null;
}
async one() {
return this.firstOrNull();
}
async delete() {
if (this.mode !== "select") throw new Error("delete() is only applicable in select mode.");
const table = this.ensureTable();
return this.db._deleteByQuery(table, this.toSelectQuery(), this.partitionValue);
}
async update() {
if (this.mode !== "update") throw new Error("Call setUpdates(...) before update().");
const table = this.ensureTable();
const update = {
type: "UpdateQuery",
conditions: this.conditions,
updates: this.updates ?? {},
sort: this.sort,
limit: this.limitValue,
partition: this.partitionValue ?? null
};
return this.db._update(table, update, this.partitionValue);
}
onItemAdded(listener) {
this.onItemAddedListener = listener;
return this;
}
onItemUpdated(listener) {
this.onItemUpdatedListener = listener;
return this;
}
onItemDeleted(listener) {
this.onItemDeletedListener = listener;
return this;
}
onItem(listener) {
this.onItemListener = listener;
return this;
}
async streamEventsOnly(keepAlive = true) {
return this.stream(false, keepAlive);
}
async streamWithQueryResults(keepAlive = false) {
return this.stream(true, keepAlive);
}
async stream(includeQueryResults = true, keepAlive = false) {
if (this.mode !== "select") throw new Error("Streaming is only applicable in select mode.");
const table = this.ensureTable();
return this.db._stream(table, this.toSelectQuery(), includeQueryResults, keepAlive, {
onItemAdded: this.onItemAddedListener ?? void 0,
onItemUpdated: this.onItemUpdatedListener ?? void 0,
onItemDeleted: this.onItemDeletedListener ?? void 0,
onItem: this.onItemListener ?? void 0
});
}
};
var SaveBuilderImpl = class {
db;
table;
relationships = null;
constructor(db, table) {
this.db = db;
this.table = table;
}
cascade(...relationships) {
this.relationships = relationships.flat();
return this;
}
one(entity) {
const opts = this.relationships ? { relationships: this.relationships } : void 0;
return this.db._saveInternal(this.table, entity, opts);
}
many(entities) {
const opts = this.relationships ? { relationships: this.relationships } : void 0;
return this.db._saveInternal(this.table, entities, opts);
}
};
var CascadeBuilderImpl = class {
db;
rels = null;
constructor(db) {
this.db = db;
}
cascade(...relationships) {
this.rels = relationships.flat();
return this;
}
save(table, entityOrEntities) {
const opts = this.rels ? { relationships: this.rels } : void 0;
return this.db._saveInternal(String(table), entityOrEntities, opts);
}
delete(table, primaryKey) {
const opts = this.rels ? { relationships: this.rels } : void 0;
return this.db.delete(table, primaryKey, opts);
}
};
var onyx = {
init(config) {
return new OnyxDatabaseImpl(config);
},
clearCacheConfig
};
// src/helpers/sort.ts
var asc = (field) => ({ field, order: "ASC" });
var desc = (field) => ({ field, order: "DESC" });
// src/builders/condition-builder.ts
var ConditionBuilderImpl = class {
condition;
/**
* Initialize with an optional starting criteria.
*
* @param criteria Initial query criteria to seed the builder.
* @example
* ```ts
* const builder = new ConditionBuilderImpl({ field: 'id', operator: 'eq', value: '1' });
* ```
*/
constructor(criteria = null) {
this.condition = criteria ? this.single(criteria) : null;
}
/**
* Add a criteria combined with AND.
*
* @param condition Another builder or raw criteria to AND.
* @example
* ```ts
* builder.and({ field: 'name', operator: 'eq', value: 'Ada' });
* ```
*/
and(condition) {
this.addCompound("AND", this.prepare(condition));
return this;
}
/**
* Add a criteria combined with OR.
*
* @param condition Another builder or raw criteria to OR.
* @example
* ```ts
* builder.or({ field: 'status', operator: 'eq', value: 'active' });
* ```
*/
or(condition) {
this.addCompound("OR", this.prepare(condition));
return this;
}
/**
* Produce the composed QueryCondition.
*
* @example
* ```ts
* const condition = builder.toCondition();
* ```
*/
toCondition() {
if (!this.condition) {
throw new Error("ConditionBuilder has no criteria.");
}
return this.condition;
}
/**
* Wrap raw criteria into a single condition object.
*
* @param criteria Criteria to wrap.
* @example
* ```ts
* builder['single']({ field: 'id', operator: 'eq', value: '1' });
* ```
*/
single(criteria) {
return { conditionType: "SingleCondition", criteria };
}
/**
* Create a compound condition using the provided operator.
*
* @param operator Logical operator to apply.
* @param conditions Child conditions to combine.
* @example
* ```ts
* builder['compound']('AND', [condA, condB]);
* ```
*/
compound(operator, conditions) {
return { conditionType: "CompoundCondition", operator, conditions };
}
/**
* Merge the next condition into the existing tree using the operator.
*
* @param operator Logical operator for the merge.
* @param next Condition to merge into the tree.
* @example
* ```ts
* builder['addCompound']('AND', someCondition);
* ```
*/
addCompound(operator, next) {
if (!this.condition) {
this.condition = next;
return;
}
if (this.condition.conditionType === "CompoundCondition" && this.condition.operator === operator) {
this.condition.conditions.push(next);
return;
}
this.condition = this.compound(operator, [this.condition, next]);
}
/**
* Normalize input into a QueryCondition instance.
*
* @param condition Builder or raw criteria to normalize.
* @example
* ```ts
* const qc = builder['prepare']({ field: 'id', operator: 'eq', value: '1' });
* ```
*/
prepare(condition) {
if (typeof condition.toCondition === "function") {
return condition.toCondition();
}
const c2 = condition;
if (c2 && typeof c2.field === "string" && typeof c2.operator === "string") {
return this.single(c2);
}
throw new Error("Invalid condition");
}
};
// src/helpers/conditions.ts
var c = (field, operator, value) => new ConditionBuilderImpl({ field, operator, value });
var eq = (field, value) => c(field, "EQUAL", value);
var neq = (field, value) => c(field, "NOT_EQUAL", value);
var inOp = (field, values) => c(
field,
"IN",
typeof values === "string" ? values.split(",").map((v) => v.trim()).filter((v) => v.length) : values
);
var notIn = (field, values) => c(field, "NOT_IN", values);
var between = (field, lower2, upper2) => c(field, "BETWEEN", [lower2, upper2]);
var gt = (field, value) => c(field, "GREATER_THAN", value);
var gte = (field, value) => c(field, "GREATER_THAN_EQUAL", value);
var lt = (field, value) => c(field, "LESS_THAN", value);
var lte = (field, value) => c(field, "LESS_THAN_EQUAL", value);
var matches = (field, regex) => c(field, "MATCHES", regex);
var notMatches = (field, regex) => c(field, "NOT_MATCHES", regex);
var like = (field, pattern) => c(field, "LIKE", pattern);
var notLike = (field, pattern) => c(field, "NOT_LIKE", pattern);
var contains = (field, value) => c(field, "CONTAINS", value);
var containsIgnoreCase = (field, value) => c(field, "CONTAINS_IGNORE_CASE", value);
var notContains = (field, value) => c(field, "NOT_CONTAINS", value);
var notContainsIgnoreCase = (field, value) => c(field, "NOT_CONTAINS_IGNORE_CASE", value);
var startsWith = (field, prefix) => c(field, "STARTS_WITH", prefix);
var notStartsWith = (field, prefix) => c(field, "NOT_STARTS_WITH", prefix);
var isNull = (field) => c(field, "IS_NULL");
var notNull = (field) => c(field, "NOT_NULL");
// src/helpers/aggregates.t