UNPKG

@smartsamurai/krapi-sdk

Version:

KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)

1,560 lines (1,549 loc) 92.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/client.ts var client_exports = {}; __export(client_exports, { KrapiClient: () => KrapiClient, default: () => client_default }); module.exports = __toCommonJS(client_exports); // src/client/backup-api-manager.ts var BackupApiManager = class { constructor(axiosInstance) { this.axiosInstance = axiosInstance; } /** * Create a project backup */ async createProject(projectId, options) { const response = await this.axiosInstance.post(`/krapi/k1/projects/${projectId}/backup`, options || {}); return response.data; } /** * Restore a project from backup */ async restoreProject(projectId, backupId, password, options) { const response = await this.axiosInstance.post(`/krapi/k1/projects/${projectId}/restore`, { backup_id: backupId, password, ...options }); return response.data; } /** * List backups */ async list(projectId, type) { const url = projectId ? `/krapi/k1/projects/${projectId}/backups${type ? `?type=${type}` : ""}` : `/krapi/k1/backups${type ? `?type=${type}` : ""}`; const response = await this.axiosInstance.get( url ); return response.data; } /** * Delete a backup */ async delete(backupId) { const response = await this.axiosInstance.delete(`/krapi/k1/backups/${backupId}`); return response.data; } /** * Create a system backup */ async createSystem(options) { const response = await this.axiosInstance.post("/krapi/k1/backup/system", options || {}); return response.data; } }; // src/utils/response-normalizer.ts var ResponseNormalizer = class { /** * Normalize document list responses from different API formats */ static normalizeDocumentListResponse(response) { if (!response) return []; if (Array.isArray(response)) { return response; } if (typeof response === "object" && response !== null) { if ("data" in response) { const data = response.data; if (Array.isArray(data)) { return data; } } if ("documents" in response) { const documents = response.documents; if (Array.isArray(documents)) { return documents; } } } return []; } /** * Normalize single document responses */ static normalizeDocumentResponse(response) { if (!response) return {}; if (typeof response === "object" && response !== null) { if ("data" in response) { const data = response.data; if (data && typeof data === "object") { return data; } } return response; } return {}; } /** * Normalize collection responses */ static normalizeCollectionResponse(response) { if (!response) return {}; if (typeof response === "object" && response !== null) { if ("data" in response) { const data = response.data; if (data && typeof data === "object") { return data; } } return response; } return {}; } }; // src/client/collections-api-manager.ts var CollectionsApiManager = class { constructor(axiosInstance, collectionsClient) { /** * Documents API methods */ this.documents = { /** * List documents in a collection */ list: async (projectId, collectionName, options) => { const url = `/projects/${projectId}/collections/${collectionName}/documents`; const params = new URLSearchParams(); if (options?.limit) params.append("limit", options.limit.toString()); if (options?.offset) params.append("offset", options.offset.toString()); if (options?.filter) params.append("filter", JSON.stringify(options.filter)); if (options?.sort) params.append("sort", JSON.stringify(options.sort)); const response = await this.axiosInstance.get( `/krapi/k1${url}${params.toString() ? `?${params.toString()}` : ""}` ); const normalizedData = ResponseNormalizer.normalizeDocumentListResponse(response.data); return { success: true, data: normalizedData }; }, /** * Get a single document */ get: async (projectId, collectionName, documentId) => { const response = await this.collectionsClient.getDocument(projectId, collectionName, documentId); const result = { success: response.success, data: ResponseNormalizer.normalizeDocumentResponse(response.data) }; if (response.error) { result.error = response.error; } return result; }, /** * Create a new document */ create: async (projectId, collectionName, data) => { const response = await this.axiosInstance.post( `/krapi/k1/projects/${projectId}/collections/${collectionName}/documents`, { data, created_by: "client" } ); const normalized = ResponseNormalizer.normalizeDocumentResponse(response.data); return { success: true, data: normalized }; }, /** * Update a document */ update: async (projectId, collectionName, documentId, data) => { return this.collectionsClient.updateDocument( projectId, collectionName, documentId, { data, updated_by: "client" } ); }, /** * Delete a document */ delete: async (projectId, collectionName, documentId) => { return this.collectionsClient.deleteDocument(projectId, collectionName, documentId); }, /** * Bulk create documents */ bulkCreate: async (projectId, collectionName, documents) => { const createRequests = documents.map((doc) => ({ data: doc, created_by: "client" })); return this.collectionsClient.bulkCreateDocuments(projectId, collectionName, createRequests); }, /** * Bulk update documents with filter */ bulkUpdate: async (projectId, collectionName, filter, updates) => { const response = await this.axiosInstance.put( `/krapi/k1/projects/${projectId}/collections/${collectionName}/documents/bulk`, { filter, updates } ); return response.data; }, /** * Bulk delete documents with filter */ bulkDelete: async (projectId, collectionName, filter) => { const response = await this.axiosInstance.post( `/krapi/k1/projects/${projectId}/collections/${collectionName}/documents/bulk-delete`, { filter } ); return response.data; }, /** * Search documents */ search: async (projectId, collectionName, query, options) => { const searchOptions = { text: query }; if (options?.fields !== void 0) searchOptions.fields = options.fields; if (options?.limit !== void 0) searchOptions.limit = options.limit; return this.collectionsClient.searchDocuments(projectId, collectionName, searchOptions); }, /** * Aggregate documents */ aggregate: async (projectId, collectionName, options) => { const aggregations = {}; if (options.operations) { for (const op of options.operations) { const opType = op.operation; if (["count", "sum", "avg", "min", "max"].includes(op.operation)) { aggregations[op.field] = { type: opType, field: op.field }; } } } const aggregateOptions = { aggregations }; if (options.groupBy) { aggregateOptions.group_by = [options.groupBy]; } return this.collectionsClient.aggregateDocuments(projectId, collectionName, aggregateOptions); } }; /** * Collection management methods */ this.collections = { /** * List collections in a project */ list: async (projectId) => { const result = await this.collectionsClient.getProjectCollections(projectId); if (result && typeof result === "object") { if ("collections" in result && Array.isArray(result.collections)) { return { success: true, data: result.collections }; } if ("data" in result && Array.isArray(result.data)) { return { success: true, data: result.data }; } } return { success: true, data: [] }; }, /** * Get a single collection */ get: async (projectId, collectionName) => { return this.collectionsClient.getCollection(projectId, collectionName); }, /** * Create a new collection */ create: async (projectId, collectionData) => { const response = await this.collectionsClient.createCollection( projectId, collectionData ); const normalizedData = ResponseNormalizer.normalizeCollectionResponse(response.data); const result = { success: response.success, data: normalizedData }; if (response.error) { result.error = response.error; } return result; }, /** * Update a collection */ update: async (projectId, collectionName, updates) => { return this.collectionsClient.updateCollection(projectId, collectionName, updates); }, /** * Delete a collection */ delete: async (projectId, collectionName) => { return this.collectionsClient.deleteCollection(projectId, collectionName); } }; this.axiosInstance = axiosInstance; this.collectionsClient = collectionsClient; } }; // src/core/krapi-error.ts var KrapiError = class _KrapiError extends Error { /** * Create a new KrapiError instance * * @param {string} message - Error message * @param {ErrorCode} [code] - Error code * @param {number} [status] - HTTP status code * @param {Record<string, unknown>} [details] - Additional error details * @param {string} [requestId] - Request ID for tracking * @param {unknown} [cause] - Original error that caused this error */ constructor(message, code = "INTERNAL_ERROR", status, details, requestId, cause) { super(message); this.name = "KrapiError"; this.code = code; if (status !== void 0) this.status = status; if (details !== void 0) this.details = details; if (requestId !== void 0) this.requestId = requestId; if (cause !== void 0) this.cause = cause; this.timestamp = (/* @__PURE__ */ new Date()).toISOString(); if (Error.captureStackTrace) { Error.captureStackTrace(this, _KrapiError); } } // Static factory methods /** * Create KrapiError from HttpError * * @param httpError - HttpError instance to convert * @param context - Additional context to include * @returns New KrapiError instance */ static fromHttpError(httpError, context) { let errorCode = "INTERNAL_ERROR"; if (httpError.status) { switch (httpError.status) { case 400: errorCode = "BAD_REQUEST"; break; case 401: errorCode = "UNAUTHORIZED"; break; case 403: errorCode = "FORBIDDEN"; break; case 404: errorCode = "NOT_FOUND"; break; case 409: errorCode = "CONFLICT"; break; case 422: errorCode = "UNPROCESSABLE_ENTITY"; break; case 429: errorCode = "RATE_LIMIT_EXCEEDED"; break; case 500: errorCode = "SERVER_ERROR"; break; case 502: case 503: errorCode = "SERVICE_UNAVAILABLE"; break; case 408: errorCode = "TIMEOUT"; break; } } return new _KrapiError( httpError.message, errorCode, httpError.status, { httpError: { code: httpError.code, responseData: httpError.responseData }, ...context } ); } /** * Create KrapiError from generic Error * * @param error - Generic error to convert * @param defaultCode - Default error code if cannot be inferred * @param context - Additional context to include * @returns New KrapiError instance */ static fromError(error, defaultCode = "INTERNAL_ERROR", context) { const code = getErrorCodeFromMessage(error.message) || defaultCode; return new _KrapiError( error.message, code, void 0, { originalStack: error.stack, ...context }, void 0, error ); } /** * Create validation error * * @param message - Error message * @param field - Field that failed validation * @param value - Invalid value (will be masked for security) * @param context - Additional context * @returns New validation error */ static validationError(message, field, value, context) { return new _KrapiError( message, "VALIDATION_ERROR", 400, { field, value: maskSensitiveValue(value), ...context } ); } /** * Create not found error * * @param message - Error message or resource type * @param context - Additional context (optional) * @returns New not found error */ static notFound(message, context) { return new _KrapiError( message, "NOT_FOUND", 404, context ); } /** * Create authentication error * * @param message - Error message * @param context - Additional context * @returns New authentication error */ static authError(message = "Authentication required", context) { return new _KrapiError( message, "UNAUTHORIZED", 401, context ); } /** * Create authorization error * * @param message - Error message * @param context - Additional context * @returns New authorization error */ static forbidden(message = "Access forbidden", context) { return new _KrapiError( message, "FORBIDDEN", 403, context ); } /** * Create conflict error * * @param message - Error message * @param context - Additional context * @returns New conflict error */ static conflict(message, context) { return new _KrapiError( message, "CONFLICT", 409, context ); } /** * Create internal server error * * @param message - Error message * @param context - Additional context * @returns New internal server error */ static internalError(message = "Internal server error", context) { return new _KrapiError( message, "INTERNAL_ERROR", 500, context ); } /** * Create bad request error * * @param message - Error message * @param context - Additional context * @returns New bad request error */ static badRequest(message, context) { return new _KrapiError( message, "BAD_REQUEST", 400, context ); } /** * Create service unavailable error * * @param message - Error message * @param context - Additional context * @returns New service unavailable error */ static serviceUnavailable(message = "Service unavailable", context) { return new _KrapiError( message, "SERVICE_UNAVAILABLE", 503, context ); } // Instance methods /** * Create a new error with additional context * * @param context - Additional context to merge * @returns New KrapiError with merged context */ withContext(context) { return new _KrapiError( this.message, this.code, this.status, { ...this.details || {}, ...context }, this.requestId, this.cause ); } /** * Check if this is a validation error * * @returns True if this is a validation error */ isValidationError() { return this.code === "VALIDATION_ERROR"; } /** * Check if this is a not found error * * @returns True if this is a not found error */ isNotFound() { return this.code === "NOT_FOUND"; } /** * Check if this is an authentication error * * @returns True if this is an authentication error */ isAuthError() { return this.code === "UNAUTHORIZED" || this.code === "FORBIDDEN"; } /** * Check if this is a client error (4xx) * * @returns True if this is a client error */ isClientError() { return this.status !== void 0 && this.status >= 400 && this.status < 500; } /** * Check if this is a server error (5xx) * * @returns True if this is a server error */ isServerError() { return this.status !== void 0 && this.status >= 500; } /** * Convert error to JSON format * * @returns {Object} Error as JSON object */ toJSON() { const result = { code: this.code, message: this.message, timestamp: this.timestamp }; if (this.status !== void 0) result.status = this.status; if (this.details !== void 0) result.details = this.details; if (this.requestId !== void 0) result.request_id = this.requestId; if (this.cause !== void 0) { result.cause = this.cause instanceof Error ? this.cause.message : String(this.cause); } return result; } /** * Get detailed error message * * @returns {string} Detailed error message */ getDetailedMessage() { let message = this.message; if (this.code) { message = `[${this.code}] ${message}`; } if (this.status) { message = `${message} (HTTP ${this.status})`; } return message; } }; function maskSensitiveValue(value) { if (typeof value === "string") { if (value.length > 10 && (value.toLowerCase().includes("password") || value.toLowerCase().includes("token") || value.toLowerCase().includes("secret") || value.toLowerCase().includes("key"))) { return `${value.substring(0, 4)}****`; } if (value.length > 50) { return `${value.substring(0, 20)}...[${value.length - 40} chars]...${value.substring(value.length - 20)}`; } } return value; } function getErrorCodeFromMessage(message) { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes("unauthorized") || lowerMessage.includes("not authorized")) { return "UNAUTHORIZED"; } if (lowerMessage.includes("forbidden") || lowerMessage.includes("permission denied")) { return "FORBIDDEN"; } if (lowerMessage.includes("invalid credentials") || lowerMessage.includes("authentication failed")) { return "UNAUTHORIZED"; } if (lowerMessage.includes("not found") || lowerMessage.includes("does not exist")) { return "NOT_FOUND"; } if (lowerMessage.includes("already exists") || lowerMessage.includes("duplicate")) { return "CONFLICT"; } if (lowerMessage.includes("validation") || lowerMessage.includes("invalid") || lowerMessage.includes("required") || lowerMessage.includes("missing")) { return "VALIDATION_ERROR"; } if (lowerMessage.includes("network") || lowerMessage.includes("connection") || lowerMessage.includes("timeout")) { return "NETWORK_ERROR"; } if (lowerMessage.includes("rate limit")) { return "RATE_LIMIT_EXCEEDED"; } if (lowerMessage.includes("internal server") || lowerMessage.includes("database error") || lowerMessage.includes("query failed")) { return "INTERNAL_ERROR"; } if (lowerMessage.includes("service unavailable") || lowerMessage.includes("temporarily unavailable")) { return "SERVICE_UNAVAILABLE"; } if (lowerMessage.includes("bad request")) { return "BAD_REQUEST"; } return void 0; } // src/http-clients/http-error.ts var HttpError = class _HttpError extends Error { /** * Create a new HttpError instance * * @param {string} message - Error message * @param {Object} options - Error options * @param {number} [options.status] - HTTP status code * @param {string} [options.method] - HTTP method * @param {string} [options.url] - Request URL * @param {Record<string, string>} [options.requestHeaders] - Request headers * @param {unknown} [options.requestBody] - Request body/data sent * @param {Record<string, unknown>} [options.requestQuery] - Request query parameters * @param {unknown} [options.responseData] - Response data * @param {Record<string, string>} [options.responseHeaders] - Response headers * @param {string} [options.code] - Error code * @param {unknown} [options.originalError] - Original error */ constructor(message, options = {}) { super(message); this.name = "HttpError"; if (options.status !== void 0) { this.status = options.status; } if (options.method !== void 0) { this.method = options.method; } if (options.url !== void 0) { this.url = options.url; } if (options.requestHeaders !== void 0) { this.requestHeaders = options.requestHeaders; } if (options.requestBody !== void 0) { this.requestBody = options.requestBody; } if (options.requestQuery !== void 0) { this.requestQuery = options.requestQuery; } if (options.responseData !== void 0) { this.responseData = options.responseData; } if (options.responseHeaders !== void 0) { this.responseHeaders = options.responseHeaders; } if (options.code !== void 0) { this.code = options.code; } if (options.originalError !== void 0) { this.originalError = options.originalError; } this.isApiError = options.status !== void 0 && options.status >= 400; this.isNetworkError = options.status === void 0; this.isAuthError = options.status === 401 || options.status === 403; this.isClientError = options.status !== void 0 && options.status >= 400 && options.status < 500; this.isServerError = options.status !== void 0 && options.status >= 500; if (Error.captureStackTrace) { Error.captureStackTrace(this, _HttpError); } } /** * Get a detailed error message with all available information * * @returns {string} Detailed error message */ getDetailedMessage() { const parts = [this.message]; if (this.status) { parts.push(`(HTTP ${this.status})`); } if (this.method && this.url) { parts.push(`[${this.method} ${this.url}]`); } if (this.code) { parts.push(`[Code: ${this.code}]`); } if (this.responseData && typeof this.responseData === "object") { const response = this.responseData; if (response.error && typeof response.error === "string") { parts.push(`Backend error: ${response.error}`); } else if (response.message && typeof response.message === "string") { parts.push(`Backend message: ${response.message}`); } } return parts.join(" "); } /** * Convert error to JSON for logging * * @returns {Record<string, unknown>} JSON representation */ toJSON() { return { name: this.name, message: this.message, status: this.status, method: this.method, url: this.url, code: this.code, isApiError: this.isApiError, isNetworkError: this.isNetworkError, isAuthError: this.isAuthError, isClientError: this.isClientError, isServerError: this.isServerError, requestHeaders: this.requestHeaders, requestBody: this.requestBody, requestQuery: this.requestQuery, responseData: this.responseData, responseHeaders: this.responseHeaders, originalError: this.originalError, stack: this.stack }; } }; // src/utils/error-handler.ts function normalizeError(error, defaultCode = "INTERNAL_ERROR", context) { if (error instanceof KrapiError) { return context ? enrichError(error, context) : error; } if (error instanceof HttpError) { return transformHttpError(error, context); } if (error instanceof Error) { const code = getErrorCodeFromMessage2(error.message); return new KrapiError( error.message, code, void 0, { ...context, originalError: error, stack: error.stack } ); } if (typeof error === "string") { const code = getErrorCodeFromMessage2(error); return new KrapiError( error, code, void 0, context ); } return new KrapiError( "An unexpected error occurred", defaultCode, void 0, { ...context, originalError: error } ); } function transformHttpError(httpError, context) { const errorCode = getErrorCodeFromStatus(httpError.status); let message = httpError.message; if (httpError.status && !message.includes(`HTTP ${httpError.status}`)) { message = `${message} (HTTP ${httpError.status})`; } const krapiError = new KrapiError( message, errorCode, httpError.status, { ...context, httpError: { method: httpError.method, url: httpError.url, status: httpError.status, code: httpError.code, isApiError: httpError.isApiError, isNetworkError: httpError.isNetworkError, isAuthError: httpError.isAuthError, isClientError: httpError.isClientError, isServerError: httpError.isServerError, // Request context - what was sent requestHeaders: httpError.requestHeaders, requestBody: httpError.requestBody, requestQuery: httpError.requestQuery, // Response context - what was received responseData: httpError.responseData, responseHeaders: httpError.responseHeaders }, originalError: httpError } ); return krapiError; } function enrichError(error, context) { const mergedDetails = { ...error.details || {}, ...context }; if (error.details?.originalError) { mergedDetails.originalError = error.details.originalError; } return new KrapiError( error.message, error.code, error.status, mergedDetails, error.requestId, error.cause ); } function getErrorCodeFromStatus(status) { if (!status) { return "NETWORK_ERROR"; } switch (status) { case 400: return "BAD_REQUEST"; case 401: return "UNAUTHORIZED"; case 403: return "FORBIDDEN"; case 404: return "NOT_FOUND"; case 409: return "CONFLICT"; case 422: return "UNPROCESSABLE_ENTITY"; case 429: return "RATE_LIMIT_EXCEEDED"; case 500: return "SERVER_ERROR"; case 502: case 503: case 504: return "SERVICE_UNAVAILABLE"; case 408: return "TIMEOUT"; default: if (status >= 400 && status < 500) { return "BAD_REQUEST"; } if (status >= 500) { return "SERVER_ERROR"; } return "INTERNAL_ERROR"; } } function getErrorCodeFromMessage2(message) { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes("unauthorized") || lowerMessage.includes("not authorized")) { return "UNAUTHORIZED"; } if (lowerMessage.includes("forbidden") || lowerMessage.includes("permission denied")) { return "FORBIDDEN"; } if (lowerMessage.includes("invalid credentials") || lowerMessage.includes("authentication failed")) { return "UNAUTHORIZED"; } if (lowerMessage.includes("not found") || lowerMessage.includes("does not exist")) { return "NOT_FOUND"; } if (lowerMessage.includes("already exists") || lowerMessage.includes("duplicate")) { return "CONFLICT"; } if (lowerMessage.includes("validation") || lowerMessage.includes("invalid") || lowerMessage.includes("required") || lowerMessage.includes("missing")) { return "VALIDATION_ERROR"; } if (lowerMessage.includes("network") || lowerMessage.includes("connection") || lowerMessage.includes("timeout")) { return "NETWORK_ERROR"; } if (lowerMessage.includes("rate limit")) { return "RATE_LIMIT_EXCEEDED"; } if (lowerMessage.includes("internal server") || lowerMessage.includes("database error") || lowerMessage.includes("query failed")) { return "INTERNAL_ERROR"; } if (lowerMessage.includes("service unavailable") || lowerMessage.includes("temporarily unavailable")) { return "SERVICE_UNAVAILABLE"; } if (lowerMessage.includes("bad request")) { return "BAD_REQUEST"; } return "INTERNAL_ERROR"; } // src/client/project-api-manager.ts var ProjectApiManager = class { constructor(axiosInstance) { this.axiosInstance = axiosInstance; } /** * Get all projects * @returns {Promise<Project[]>} Array of projects */ async getAll() { try { const response = await this.axiosInstance.get( "/krapi/k1/projects" ); return Array.isArray(response.data) ? response.data : []; } catch (error) { throw normalizeError(error, "INTERNAL_ERROR", { operation: "getProjects" }); } } /** * Get project by ID * @param {string} id - Project ID * @returns {Promise<Project>} Project object */ async get(id) { if (!id || typeof id !== "string") { throw KrapiError.validationError("Project ID is required", "id"); } try { const response = await this.axiosInstance.get( `/krapi/k1/projects/${id}` ); return response.data; } catch (error) { throw normalizeError(error, "INTERNAL_ERROR", { operation: "getProject", projectId: id }); } } /** * Create a new project * @param {CreateProjectRequest} data - Project data * @returns {Promise<Project>} Created project */ async create(data) { if (!data || !data.name || typeof data.name !== "string" || data.name.trim() === "") { throw KrapiError.validationError("Project name is required", "name"); } try { const response = await this.axiosInstance.post( "/krapi/k1/projects", data ); return response.data; } catch (error) { throw normalizeError(error, "INTERNAL_ERROR", { operation: "createProject" }); } } /** * Update a project * @param {string} id - Project ID * @param {UpdateProjectRequest} data - Updated project data * @returns {Promise<Project>} Updated project */ async update(id, data) { if (!id || typeof id !== "string") { throw KrapiError.validationError("Project ID is required", "id"); } if (!data || typeof data !== "object") { throw KrapiError.validationError("Update data is required", "data"); } try { const response = await this.axiosInstance.put( `/krapi/k1/projects/${id}`, data ); return response.data; } catch (error) { throw normalizeError(error, "INTERNAL_ERROR", { operation: "updateProject", projectId: id }); } } /** * Delete a project * @param {string} id - Project ID * @returns {Promise<void>} */ async delete(id) { if (!id || typeof id !== "string") { throw KrapiError.validationError("Project ID is required", "id"); } try { await this.axiosInstance.delete(`/krapi/k1/projects/${id}`); } catch (error) { throw normalizeError(error, "INTERNAL_ERROR", { operation: "deleteProject", projectId: id }); } } }; // src/http-clients/base-http-client.ts var import_axios = __toESM(require("axios")); // src/utils/endpoint-utils.ts function extractPort(endpoint) { try { const url = new URL(endpoint); if (url.port) { return parseInt(url.port, 10); } return url.protocol === "https:" ? 443 : 80; } catch { return null; } } function isBackendUrl(endpoint) { const port = extractPort(endpoint); return port === 3470; } // src/http-clients/base-http-client.ts var BaseHttpClient = class { /** * Create a new BaseHttpClient instance * * @param {HttpClientConfig} config - HTTP client configuration */ constructor(config) { this.baseUrl = config.baseUrl.replace(/\/$/, ""); if (config.apiKey) this.apiKey = config.apiKey; if (config.sessionToken) this.sessionToken = config.sessionToken; if (config.projectId) this.projectId = config.projectId; if (config.retry) this.retryConfig = config.retry; } /** * Initialize the HTTP client with interceptors * * Sets up axios instance with authentication interceptors and error handling. * * @returns {Promise<void>} * * @example * await client.initializeClient(); */ async initializeClient() { if (this.httpClient !== void 0) return; let baseURL = this.baseUrl; const isBackend = isBackendUrl(baseURL); const hasApiPath = /\/api\/krapi\/k1(\/|$)/.test(baseURL); const hasKrapiPath = /\/krapi\/k1(\/|$)/.test(baseURL) && !hasApiPath; if (hasApiPath) { } else if (hasKrapiPath) { if (isBackend) { } else { baseURL = baseURL.replace("/krapi/k1", "/api/krapi/k1"); } } else { if (isBackend) { baseURL = `${baseURL}/krapi/k1`; } else { baseURL = `${baseURL}/api/krapi/k1`; } } this.httpClient = import_axios.default.create({ baseURL, timeout: 3e4, headers: { "Content-Type": "application/json" } }); this.httpClient.interceptors.request.use( (config) => { if (config.data instanceof FormData) { delete config.headers["Content-Type"]; delete config.headers["content-type"]; } const token = this.getCurrentToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; delete config.headers["X-API-Key"]; } if (config.url) { let normalizedPath = config.url; normalizedPath = normalizedPath.replace(/^(\/api)?\/krapi\/k1/, ""); const isProjectListOrCreate = /^\/projects\/?(\?|$)/.test( normalizedPath ); const isProjectScoped = /^\/projects\/[^/]+/.test(normalizedPath); if (isProjectListOrCreate) { delete config.headers["X-Project-ID"]; delete config.headers["x-project-id"]; } else if (isProjectScoped && this.projectId) { config.headers["X-Project-ID"] = this.projectId; } } if (process.env.NODE_ENV === "development" || process.env.DEBUG_SDK_HEADERS) { const normalizedPath = config.url?.replace(/^(\/api)?\/krapi\/k1/, "") || ""; const isProjectListOrCreate = /^\/projects(\/|\?|$)/.test( normalizedPath ); const isProjectScoped = /^\/projects\/[^/]+/.test(normalizedPath); const hasProjectIdHeader = !!(config.headers["X-Project-ID"] || config.headers["x-project-id"]); const relevantHeaders = Object.keys(config.headers).filter( (k) => k.toLowerCase().includes("project") || k.toLowerCase() === "authorization" ).reduce((acc, k) => { acc[k] = config.headers[k] ? "present" : "missing"; return acc; }, {}); console.log("[SDK Interceptor]", { method: config.method?.toUpperCase(), url: config.url, normalizedPath, hasProjectId: !!this.projectId, isProjectListOrCreate, isProjectScoped, willAddHeader: isProjectScoped && !!this.projectId, willRemoveHeader: isProjectListOrCreate, actualHeaderPresent: hasProjectIdHeader, relevantHeaders }); } return config; } ); this.httpClient.interceptors.response.use( (response) => response.data, // Return just the data (error) => { if ((0, import_axios.isAxiosError)(error)) { const axiosError = error; const status = axiosError.response?.status; const responseData = axiosError.response?.data; const method = axiosError.config?.method?.toUpperCase(); const relativeUrl = axiosError.config?.url; const baseUrl = axiosError.config?.baseURL || ""; const fullUrl = relativeUrl ? `${baseUrl}${relativeUrl}` : baseUrl; const requestBody = axiosError.config?.data; const requestQuery = axiosError.config?.params || {}; const responseHeaders = {}; if (axiosError.response?.headers) { Object.keys(axiosError.response.headers).forEach((key) => { const value = axiosError.response?.headers[key]; if (typeof value === "string") { responseHeaders[key] = value; } else if (Array.isArray(value) && value.length > 0) { responseHeaders[key] = value.join(", "); } }); } const requestHeaders = {}; if (axiosError.config && axiosError.config.headers) { const headers = axiosError.config.headers; Object.keys(headers).forEach((key) => { const value = headers[key]; if (typeof value === "string") { if (key.toLowerCase() === "authorization") { requestHeaders[key] = `${value.substring(0, 20)}...`; } else { requestHeaders[key] = value; } } }); } let errorMessage = "HTTP request failed"; let errorCode; if (status) { if (responseData) { if (typeof responseData === "object" && responseData !== null) { const data = responseData; errorMessage = data.error || data.message || `HTTP ${status} ${axiosError.response?.statusText || "Error"}`; if (data.code && typeof data.code === "string") { errorCode = data.code; } } else if (typeof responseData === "string") { errorMessage = responseData; } } else { errorMessage = `HTTP ${status} ${axiosError.response?.statusText || "Error"}`; } if (status === 404) { const isBackend2 = isBackendUrl(baseUrl); if (isBackend2) { errorMessage += ` - Backend URL detected (port 3470) - SDK should use /krapi/k1 path - Verify the endpoint path is correct - Check that backend routes are accessible at /krapi/k1/...`; } else { errorMessage += ` - Frontend URL detected (port 3498) - SDK should use /api/krapi/k1 path - The SDK should automatically append /api/krapi/k1/ to your endpoint - Verify the endpoint path is correct`; } } else if (status === 401) { const currentToken = this.getCurrentToken(); const isSessionToken = !!this.sessionToken; if (isSessionToken) { errorMessage += ` - Invalid or expired session token - Verify the session token is correct - Check if the session has expired - Ensure you're logged in and the session is active - Try logging in again to get a new session token`; } else if (currentToken) { errorMessage += ` - Invalid or expired API key - Check that your API key is correct - Verify the API key has the required scopes - Ensure the API key hasn't been revoked`; } else { errorMessage += ` - Authentication required - No session token or API key provided - Set a session token using setSessionToken() or provide an API key`; } } else if (status === 403) { const isSessionToken = !!this.sessionToken; if (isSessionToken) { errorMessage += ` - Your session token may not have permission for this operation - Check the user's role and permissions - Verify the session token belongs to a user with sufficient access - Ensure you're using the correct authentication method`; } else { errorMessage += ` - Your API key may not have permission for this operation - Check the API key scopes and permissions - Verify you're using the correct authentication method`; } } const httpErrorOptions = {}; if (status !== void 0) httpErrorOptions.status = status; if (method !== void 0) httpErrorOptions.method = method; if (fullUrl !== void 0) httpErrorOptions.url = fullUrl; if (Object.keys(requestHeaders).length > 0) httpErrorOptions.requestHeaders = requestHeaders; if (requestBody !== void 0) httpErrorOptions.requestBody = requestBody; if (Object.keys(requestQuery).length > 0) httpErrorOptions.requestQuery = requestQuery; if (responseData !== void 0) httpErrorOptions.responseData = responseData; if (Object.keys(responseHeaders).length > 0) httpErrorOptions.responseHeaders = responseHeaders; if (errorCode !== void 0) httpErrorOptions.code = errorCode; if (error !== void 0) httpErrorOptions.originalError = error; const httpError2 = new HttpError(errorMessage, httpErrorOptions); return Promise.reject(httpError2); } else { const networkDisplayUrl = relativeUrl ? `${baseUrl.replace(/^https?:\/\/[^/]+/, "")}${relativeUrl}` : fullUrl || "endpoint"; if (axiosError.code === "ECONNABORTED" || axiosError.message.includes("timeout")) { errorMessage = `Request timeout: ${method || "Request"} to ${networkDisplayUrl} exceeded ${axiosError.config?.timeout || 3e4}ms`; errorCode = "TIMEOUT"; } else if (axiosError.code === "ENOTFOUND" || axiosError.code === "ECONNREFUSED") { const isBackend2 = isBackendUrl(baseUrl); const connectionType = isBackend2 ? "backend URL (port 3470)" : "frontend URL (port 3498)"; errorMessage = `Cannot connect to Krapi Server at ${networkDisplayUrl}. - Is the server running? - Are you using the ${connectionType}? - Check firewall settings if accessing remotely.`; errorCode = "NETWORK_ERROR"; } else if (axiosError.code === "ECONNRESET" || axiosError.message.includes("socket hang up")) { errorMessage = `Connection reset by server at ${networkDisplayUrl}. - The server may have closed the connection unexpectedly - This can happen with long-running queries or large result sets - Try reducing query scope or increasing timeout if applicable - Check server logs for errors`; errorCode = "CONNECTION_RESET"; } else { errorMessage = `Network error: ${axiosError.message || "Failed to connect to server"} - Verify the endpoint URL is correct - Check network connectivity - Ensure the server is accessible`; errorCode = "NETWORK_ERROR"; } const requestBody2 = axiosError.config?.data; const requestQuery2 = axiosError.config?.params || {}; const httpErrorOptions = {}; if (method !== void 0) httpErrorOptions.method = method; if (fullUrl !== void 0) httpErrorOptions.url = fullUrl; if (Object.keys(requestHeaders).length > 0) httpErrorOptions.requestHeaders = requestHeaders; if (requestBody2 !== void 0) httpErrorOptions.requestBody = requestBody2; if (Object.keys(requestQuery2).length > 0) httpErrorOptions.requestQuery = requestQuery2; if (errorCode !== void 0) httpErrorOptions.code = errorCode; if (error !== void 0) httpErrorOptions.originalError = error; const httpError2 = new HttpError(errorMessage, httpErrorOptions); return Promise.reject(httpError2); } } if (error instanceof Error) { const httpError2 = new HttpError(error.message, { originalError: error, code: "UNKNOWN_ERROR" }); return Promise.reject(httpError2); } const httpError = new HttpError("Unknown error occurred", { originalError: error, code: "UNKNOWN_ERROR" }); return Promise.reject(httpError); } ); } /** * Get current authentication token (session token or API key) * * This method is used by the request interceptor to get the current token * at request time, ensuring we always use the latest token value. * * @returns {string | undefined} Current token or undefined if none set * @private */ getCurrentToken() { return this.sessionToken || this.apiKey; } /** * Set project ID for project-scoped operations * * The project ID will be automatically added as X-Project-ID header * only for project-scoped routes (e.g., /projects/{id}/...). * It will NOT be added for list/create operations (e.g., /projects). * * @param {string} projectId - Project ID * @returns {void} * * @example * client.setProjectId('project-id-here'); */ setProjectId(projectId) { this.projectId = projectId; } /** * Set session token for authentication * * @param {string} token - Session token * @returns {void} * @throws {Error} If HTTP client is not initialized * * @example * client.setSessionToken('session-token-here'); */ // Authentication methods setSessionToken(token) { if (!this.httpClient) { throw KrapiError.serviceUnavailable( "HTTP client not initialized. Call initializeClient() first or ensure connect() was called." ); } this.sessionToken = token; delete this.apiKey; this.httpClient.defaults.headers.common["Authorization"] = `Bearer ${token}`; delete this.httpClient.defaults.headers.common["X-API-Key"]; } /** * Set API key for authentication * * @param {string} key - API key * @returns {void} * * @example * client.setApiKey('api-key-here'); */ setApiKey(key) { if (!this.httpClient) { throw KrapiError.serviceUnavailable( "HTTP client not initialized. Call initializeClient() first or ensure connect() was called." ); } this.apiKey = key; delete this.sessionToken; this.httpClient.defaults.headers.common["Authorization"] = `Bearer ${key}`; delete this.httpClient.defaults.headers.common["X-API-Key"]; } /** * Clear authentication credentials * * Removes both session token and API key. * * @returns {void} * * @example * client.clearAuth(); */ clearAuth() { delete this.sessionToken; delete this.apiKey; } /** * Send GET request * * @template T * @param {string} endpoint - API endpoint * @param {QueryOptions} [params] - Query parameters * @returns {Promise<ApiResponse<T>>} API response * * @example * const response = await client.get('/projects', { limit: 10 }); */ /** * Execute request with retry logic if configured * * @template T * @param {() => Promise<T>} requestFn - Request function to execute * @returns {Promise<T>} Request result */ async executeWithRetry(requestFn) { if (!this.retryConfig?.enabled) { return requestFn(); } const maxRetries = this.retryConfig.maxRetries ?? 3; const retryDelay = this.retryConfig.retryDelay ?? 1e3; const retryableStatusCodes = this.retryConfig.retryableStatusCodes ?? [ 408, 429, 500, 502, 503, 504 ]; const retryableErrorCodes = this.retryConfig.retryableErrorCodes ?? [ "TIMEOUT", "NETWORK_ERROR" ]; let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await requestFn(); } catch (error) { lastError = error; if (attempt >= maxRetries) { break; } let shouldRetry = false; if (error instanceof HttpError) { if (error.status && retryableStatusCodes.includes(error.status)) { shouldRetry = true; } if (error.code && retryableErrorCodes.includes(error.code)) { shouldRetry = true; } } else if (error instanceof Error) { if (error.message.includes("timeout") || error.message.includes("ECONNREFUSED") || error.message.includes("ENOTFOUND")) { shouldRetry = true;