@gensx/storage
Version:
Cloud storage, blobs, sqlite, and vector database providers/hooks for GenSX.
353 lines (349 loc) • 12.6 kB
JavaScript
/**
* Check out the docs at https://www.gensx.com/docs
* Find us on Github https://github.com/gensx-inc/gensx
* Find us on Discord https://discord.gg/F5BSU8Kc
*/
import { readConfig } from '@gensx/core';
import { parseErrorResponse } from '../utils/parse-error.js';
import { USER_AGENT } from '../utils/user-agent.js';
/* eslint-disable @typescript-eslint/only-throw-error */
/**
* Base URL for the GenSX Console API
*/
const API_BASE_URL = "https://api.gensx.com";
/**
* Abstract base error class for search operations
*/
class SearchError extends Error {
code;
cause;
constructor(code, message, cause) {
super(message);
this.code = code;
this.cause = cause;
this.name = "SearchError";
}
}
/**
* Error class for API errors (bad requests, server errors, etc.)
*/
class SearchApiError extends SearchError {
constructor(message, cause) {
super("SEARCH_ERROR", message, cause);
this.name = "SearchApiError";
}
}
/**
* Error class for network errors
*/
class SearchNetworkError extends SearchError {
constructor(message, cause) {
super("NETWORK_ERROR", message, cause);
this.name = "SearchNetworkError";
}
}
/**
* Helper to convert API errors to more specific errors
*/
function handleApiError(err, operation) {
if (err instanceof SearchError) {
throw err;
}
if (err instanceof Error) {
throw new SearchNetworkError(`Error during ${operation}: ${err.message}`, err);
}
throw new SearchNetworkError(`Error during ${operation}: ${String(err)}`);
}
/**
* Remote implementation of vector namespace
*/
class SearchNamespace {
namespaceId;
apiBaseUrl;
apiKey;
org;
project;
environment;
constructor(namespaceId, apiBaseUrl, apiKey, org, project, environment) {
this.namespaceId = namespaceId;
this.apiBaseUrl = apiBaseUrl;
this.apiKey = apiKey;
this.org = org;
this.project = project;
this.environment = environment;
}
async write({ upsertColumns, upsertRows, patchColumns, patchRows, deletes, deleteByFilter, distanceMetric, schema, }) {
try {
const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(this.namespaceId)}`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
},
body: JSON.stringify({
upsertColumns,
upsertRows,
patchColumns,
patchRows,
deletes,
deleteByFilter,
distanceMetric,
schema,
}),
});
if (!response.ok) {
const message = await parseErrorResponse(response);
throw new SearchApiError(`Failed to write: ${message}`);
}
const data = (await response.json());
return data;
}
catch (err) {
if (!(err instanceof SearchError)) {
throw handleApiError(err, "write");
}
throw err;
}
}
async query({ topK, includeAttributes, filters, rankBy, aggregateBy, consistency, }) {
try {
const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(this.namespaceId)}/query`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
},
body: JSON.stringify({
rankBy,
topK: topK,
includeAttributes,
filters,
aggregateBy,
consistency,
}),
});
if (!response.ok) {
const message = await parseErrorResponse(response);
throw new SearchApiError(`Failed to query: ${message}`);
}
const data = (await response.json());
return data;
}
catch (err) {
if (!(err instanceof SearchError)) {
throw handleApiError(err, "query");
}
throw err;
}
}
async getSchema() {
try {
const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(this.namespaceId)}/schema`, {
method: "GET",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"User-Agent": USER_AGENT,
},
});
if (!response.ok) {
const message = await parseErrorResponse(response);
throw new SearchApiError(`Failed to get schema: ${message}`);
}
const data = (await response.json());
return data;
}
catch (err) {
if (!(err instanceof SearchError)) {
throw handleApiError(err, "schema");
}
throw err;
}
}
async updateSchema({ schema }) {
try {
const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(this.namespaceId)}/schema`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
},
body: JSON.stringify(schema),
});
if (!response.ok) {
const message = await parseErrorResponse(response);
throw new SearchApiError(`Failed to update schema: ${message}`);
}
const data = (await response.json());
return data;
}
catch (err) {
if (!(err instanceof SearchError)) {
throw handleApiError(err, "updateSchema");
}
throw err;
}
}
}
/**
* Remote implementation of search
*/
class SearchStorage {
apiKey;
apiBaseUrl;
org;
project;
environment;
namespaces = new Map();
constructor(project, environment) {
this.project = project;
this.environment = environment;
const config = readConfig();
this.apiKey = process.env.GENSX_API_KEY ?? config.api?.token ?? "";
if (!this.apiKey) {
throw new Error("GENSX_API_KEY environment variable must be set for search");
}
this.org = process.env.GENSX_ORG ?? config.api?.org ?? "";
if (!this.org) {
throw new Error("Organization ID must be provided via constructor or GENSX_ORG environment variable");
}
this.apiBaseUrl =
process.env.GENSX_API_BASE_URL ?? config.api?.baseUrl ?? API_BASE_URL;
}
getNamespace(name) {
if (!this.namespaces.has(name)) {
this.namespaces.set(name, new SearchNamespace(name, this.apiBaseUrl, this.apiKey, this.org, this.project, this.environment));
}
return this.namespaces.get(name);
}
async ensureNamespace(name) {
try {
const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(name)}/ensure`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"User-Agent": USER_AGENT,
},
});
if (!response.ok) {
const message = await parseErrorResponse(response);
throw new SearchApiError(`Failed to ensure namespace: ${message}`);
}
const data = (await response.json());
// Make sure the namespace is in our cache
if (!this.namespaces.has(name)) {
this.getNamespace(name);
}
return data;
}
catch (err) {
if (!(err instanceof SearchError)) {
throw handleApiError(err, "ensureNamespace");
}
throw err;
}
}
async deleteNamespace(name) {
try {
const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(name)}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"User-Agent": USER_AGENT,
},
});
if (!response.ok) {
const message = await parseErrorResponse(response);
throw new SearchApiError(`Failed to delete namespace: ${message}`);
}
const data = (await response.json());
// Remove namespace from caches if it was successfully deleted
if (data.deleted) {
if (this.namespaces.has(name)) {
const ns = this.namespaces.get(name);
if (ns) {
this.namespaces.delete(name);
}
}
}
return data;
}
catch (err) {
if (!(err instanceof SearchError)) {
throw handleApiError(err, "deleteNamespace");
}
throw err;
}
}
async listNamespaces(options) {
try {
// Normalize prefix by removing trailing slash
const normalizedPrefix = options?.prefix?.replace(/\/$/, "");
const url = new URL(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search`);
if (normalizedPrefix) {
url.searchParams.append("prefix", normalizedPrefix);
}
if (options?.limit) {
url.searchParams.append("limit", options.limit.toString());
}
if (options?.cursor) {
url.searchParams.append("cursor", options.cursor);
}
const response = await fetch(url.toString(), {
method: "GET",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"User-Agent": USER_AGENT,
},
});
if (!response.ok) {
const message = await parseErrorResponse(response);
throw new SearchApiError(`Failed to list namespaces: ${message}`);
}
const data = (await response.json());
return {
namespaces: data.namespaces.map((ns) => ({
name: ns.name,
createdAt: new Date(ns.createdAt),
})),
nextCursor: data.nextCursor,
};
}
catch (err) {
if (!(err instanceof SearchError)) {
throw handleApiError(err, "listNamespaces");
}
throw err;
}
}
hasEnsuredNamespace(name) {
return this.namespaces.has(name);
}
/**
* Check if a namespace exists
* @param name The namespace name to check
* @returns Promise that resolves to true if the namespace exists
*/
async namespaceExists(name) {
try {
const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(name)}`, {
method: "HEAD",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"User-Agent": USER_AGENT,
},
});
return response.ok;
}
catch (err) {
if (!(err instanceof SearchError)) {
throw handleApiError(err, "namespaceExists");
}
throw err;
}
}
}
export { SearchApiError, SearchError, SearchNamespace, SearchNetworkError, SearchStorage };
//# sourceMappingURL=remote.js.map