UNPKG

@onyx.dev/onyx-database

Version:
1,590 lines (1,576 loc) 52 kB
'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