@hpkv/rest-client
Version:
A NodeJS REST client for high-performance key-value store (HPKV)
322 lines (321 loc) • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.HPKVRestClient = void 0;
/**
* Client for interacting with the HPKV REST API
*
* This client provides methods for all HPKV REST API endpoints including:
* - Record operations (get, set, delete)
* - Atomic operations (increment/decrement)
* - Range queries
* - Nexus Search and Query capabilities
*/
class HPKVRestClient {
/**
* Creates a new HPKV client
* @param baseUrl - The base URL for the HPKV REST API
* @param nexusBaseUrl - The base URL for the HPKV Nexus API
* @param apiKey - Your HPKV API key
* @throws {Error} When required parameters are missing
*/
constructor(baseUrl, nexusBaseUrl, apiKey) {
if (!baseUrl)
throw new Error("baseUrl is required");
if (!nexusBaseUrl)
throw new Error("nexusBaseUrl is required");
if (!apiKey)
throw new Error("apiKey is required");
this.baseUrl = baseUrl;
this.nexusBaseUrl = nexusBaseUrl;
this.apiKey = apiKey;
this.headers = {
"Content-Type": "application/json",
"x-api-key": this.apiKey,
};
}
/**
* Make an HTTP request to the API
* @private
* @param method - HTTP method (GET, POST, DELETE)
* @param path - API endpoint path
* @param options - Request options (body, etc.)
* @returns API response data
* @throws {ApiError} When the API returns an error
*/
async _request(method, path, options = {}) {
const baseUrl = path.startsWith("/search") || path.startsWith("/query")
? this.nexusBaseUrl
: this.baseUrl;
const url = `${baseUrl}${path}`;
const response = await fetch(url, {
method,
headers: this.headers,
...options,
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const errorData = typeof data === "object" && data !== null
? data
: {};
const error = new Error(errorData.error || `HTTP error ${response.status}`);
error.status = response.status;
error.data = errorData;
error.code = this._getErrorCode(response.status);
throw error;
}
return data;
}
/**
* Get error code based on HTTP status
* @private
* @param status - HTTP status code
* @returns Error code
*/
_getErrorCode(status) {
const errorCodes = {
400: "BAD_REQUEST",
401: "UNAUTHORIZED",
403: "FORBIDDEN",
404: "NOT_FOUND",
409: "CONFLICT",
429: "RATE_LIMIT_EXCEEDED",
500: "INTERNAL_ERROR",
};
return errorCodes[status] || "UNKNOWN_ERROR";
}
/**
* Insert or update a record
*
* This method supports three operations:
* 1. Insert a new record if the key doesn't exist
* 2. Update an existing record by completely replacing its value
* 3. Perform a partial update (append or JSON patch) when partialUpdate is true
*
* @param key - The key to store the value under
* @param value - The value to store (string or object that will be stringified)
* @param partialUpdate - If true, append to existing value or apply JSON patch if both values are valid JSON
* @returns Response with success status and message
* @throws {ApiError} When the API returns an error (400 Bad Request, 401 Unauthorized, etc.)
*
* @example
* // Insert a new record
* client.set("user:123", { name: "John", age: 30 });
*
* @example
* // Update with JSON patch (when partialUpdate=true and both values are JSON)
* client.set("user:123", { age: 31, city: "New York" }, true);
*/
async set(key, value, partialUpdate = false) {
try {
return await this._request("POST", "/record", {
body: JSON.stringify({
key,
value: typeof value === "string" ? value : JSON.stringify(value),
partialUpdate,
}),
});
}
catch (error) {
this._handleError(error);
}
}
/**
* Get a record by key
*
* Retrieves the value stored at the specified key.
*
* @param key - The key to retrieve
* @returns Response containing the key and value
* @throws {ApiError} When the API returns an error (404 Not Found if key doesn't exist)
*/
async get(key) {
try {
return await this._request("GET", `/record/${encodeURIComponent(key)}`);
}
catch (error) {
this._handleError(error);
}
}
/**
* Delete a record
*
* Removes the record stored at the specified key.
*
* @param key - The key to delete
* @returns Response with success status and message
* @throws {ApiError} When the API returns an error (404 Not Found if key doesn't exist)
*/
async delete(key) {
try {
return await this._request("DELETE", `/record/${encodeURIComponent(key)}`);
}
catch (error) {
this._handleError(error);
}
}
/**
* Increment or decrement a numeric value
*
* Atomically increments or decrements the numeric value stored at the specified key.
* This operation is useful for counters, rate limiters, and other scenarios where
* you need to ensure consistency without race conditions.
*
* @param key - The key containing the numeric value to increment/decrement
* @param increment - Value to add (positive) or subtract (negative)
* @returns Response with the new value after increment/decrement
* @throws {ApiError} When the API returns an error (404 Not Found if key doesn't exist)
*
* @example
* // Increment a counter
* const response = await client.atomicIncrement("counter:123", 1);
* console.log(response.result); // The new counter value
*
* @example
* // Decrement a counter
* const response = await client.atomicIncrement("counter:123", -1);
* console.log(response.result); // The new counter value
*/
async atomicIncrement(key, increment) {
try {
return await this._request("POST", "/record/atomic", {
body: JSON.stringify({
key,
increment,
}),
});
}
catch (error) {
this._handleError(error);
}
}
/**
* Query records within a key range
*
* Retrieves multiple records with keys that fall within the specified range.
* This is useful for retrieving related data with similar keys, such as all users
* with IDs in a certain range.
*
* @param startKey - Starting key for the range (inclusive)
* @param endKey - Ending key for the range (inclusive)
* @param limit - Maximum number of records to return (default: 100, max: 1000)
* @returns Response containing matching records, count, and truncation status
* @throws {ApiError} When the API returns an error
*/
async range(startKey, endKey, limit = 100) {
try {
const params = new URLSearchParams({
startKey: encodeURIComponent(startKey),
endKey: encodeURIComponent(endKey),
limit: limit.toString(),
});
return await this._request("GET", `/records?${params}`);
}
catch (error) {
this._handleError(error);
}
}
/**
* Perform semantic search using Nexus Search
*
* Searches for records that match the semantic meaning of the query.
*
* @param query - The search query
* @param options - Search options (topK, minScore)
* @returns Search results
* @throws {ApiError} When the API returns an error
* @throws {Error} When query is empty
*/
async nexusSearch(query, options = {}) {
try {
if (!query)
throw new Error("Query is required");
const body = {
query,
topK: Math.min(Math.max(options.topK || 5, 1), 20),
minScore: Math.min(Math.max(options.minScore || 0.5, 0), 1),
};
return await this._request("POST", "/search", {
body: JSON.stringify(body),
});
}
catch (error) {
this._handleError(error);
}
}
/**
* Get AI-generated answers using Nexus Query
*
* Retrieves AI-generated answers based on the provided query.
*
* @param query - The query for which to generate answers
* @param options - Query options (topK, minScore)
* @returns Query results with AI-generated answers
* @throws {ApiError} When the API returns an error
* @throws {Error} When query is empty
*/
async nexusQuery(query, options = {}) {
try {
if (!query)
throw new Error("Query is required");
const body = {
query,
topK: Math.min(Math.max(options.topK || 5, 1), 20),
minScore: Math.min(Math.max(options.minScore || 0.5, 0), 1),
};
return await this._request("POST", "/query", {
body: JSON.stringify(body),
});
}
catch (error) {
this._handleError(error);
}
}
/**
* Handle API errors
*
* @private
* @param error - The API error to handle
* @throws {ApiError} Enhanced error with additional context
*/
_handleError(error) {
if (error.status) {
const errorMessage = this._getErrorMessage(error.status, error.data);
const enhancedError = new Error(errorMessage);
enhancedError.status = error.status;
enhancedError.code = error.code;
enhancedError.data = error.data;
throw enhancedError;
}
else if (error.message === "Failed to fetch") {
const networkError = new Error("No response received from server");
networkError.code = "NETWORK_ERROR";
throw networkError;
}
else {
throw error;
}
}
/**
* Get human-readable error message based on status code
*
* @private
* @param status - HTTP status code
* @param data - Error data from the API
* @returns Human-readable error message
*/
_getErrorMessage(status, data) {
const messages = {
400: "Invalid parameters or request body",
401: "Missing or invalid API key",
403: "Permission denied",
404: "Record not found",
409: "Timestamp conflict",
429: "Rate limit exceeded",
500: "Server error",
};
return ((data === null || data === void 0 ? void 0 : data.error) ||
messages[status] ||
`HTTP error ${status}`);
}
}
exports.HPKVRestClient = HPKVRestClient;