UNPKG

@smartsamurai/krapi-sdk

Version:

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

547 lines (501 loc) 13.1 kB
/** * KrapiError Class * * Standard error class for KRAPI SDK with error codes and structured error information. * * @module core/krapi-error * @example * throw new KrapiError('Resource not found', 'NOT_FOUND', 404); */ export type ErrorCode = | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "VALIDATION_ERROR" | "RATE_LIMIT_EXCEEDED" | "SERVER_ERROR" | "NETWORK_ERROR" | "TIMEOUT" | "BAD_REQUEST" | "CONFLICT" | "UNPROCESSABLE_ENTITY" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE" | "REQUEST_ERROR"; /** * KrapiError Class * * Extended Error class with KRAPI-specific error information. * * @class KrapiError * @extends {Error} */ export class KrapiError extends Error { /** * Error code for programmatic error handling */ public readonly code: ErrorCode; /** * HTTP status code (if applicable) */ public readonly status?: number; /** * Additional error details */ public readonly details?: Record<string, unknown>; /** * Request ID for tracking (if available) */ public readonly requestId?: string; /** * Timestamp when error occurred */ public readonly timestamp: string; /** * Original error that was wrapped (if any) */ public readonly cause?: unknown; /** * 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: string, code: ErrorCode = "INTERNAL_ERROR", status?: number, details?: Record<string, unknown>, requestId?: string, cause?: unknown ) { super(message); this.name = "KrapiError"; this.code = code; if (status !== undefined) this.status = status; if (details !== undefined) this.details = details; if (requestId !== undefined) this.requestId = requestId; if (cause !== undefined) this.cause = cause; this.timestamp = new Date().toISOString(); // Maintains proper stack trace for where our error was thrown (only available on V8) 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: { status?: number; message: string; code?: string; responseData?: unknown }, context?: Record<string, unknown> ): KrapiError { // Map HTTP status to error code let errorCode: 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: Error, defaultCode: ErrorCode = "INTERNAL_ERROR", context?: Record<string, unknown> ): KrapiError { // Try to infer error code from message const code = getErrorCodeFromMessage(error.message) || defaultCode; return new KrapiError( error.message, code, undefined, { originalStack: error.stack, ...context, }, undefined, 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: string, field?: string, value?: unknown, context?: Record<string, unknown> ): KrapiError { 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: string, context?: Record<string, unknown> ): KrapiError { 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?: Record<string, unknown> ): KrapiError { 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?: Record<string, unknown> ): KrapiError { 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: string, context?: Record<string, unknown> ): KrapiError { 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?: Record<string, unknown> ): KrapiError { 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: string, context?: Record<string, unknown> ): KrapiError { 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?: Record<string, unknown> ): KrapiError { 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: Record<string, unknown>): KrapiError { 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(): boolean { return this.code === "VALIDATION_ERROR"; } /** * Check if this is a not found error * * @returns True if this is a not found error */ isNotFound(): boolean { return this.code === "NOT_FOUND"; } /** * Check if this is an authentication error * * @returns True if this is an authentication error */ isAuthError(): boolean { return this.code === "UNAUTHORIZED" || this.code === "FORBIDDEN"; } /** * Check if this is a client error (4xx) * * @returns True if this is a client error */ isClientError(): boolean { return this.status !== undefined && this.status >= 400 && this.status < 500; } /** * Check if this is a server error (5xx) * * @returns True if this is a server error */ isServerError(): boolean { return this.status !== undefined && this.status >= 500; } /** * Convert error to JSON format * * @returns {Object} Error as JSON object */ toJSON(): { code: ErrorCode; message: string; status?: number; details?: Record<string, unknown>; timestamp: string; request_id?: string; cause?: string; } { const result: { code: ErrorCode; message: string; status?: number; details?: Record<string, unknown>; timestamp: string; request_id?: string; cause?: string; } = { code: this.code, message: this.message, timestamp: this.timestamp, }; if (this.status !== undefined) result.status = this.status; if (this.details !== undefined) result.details = this.details; if (this.requestId !== undefined) result.request_id = this.requestId; if (this.cause !== undefined) { result.cause = this.cause instanceof Error ? this.cause.message : String(this.cause); } return result; } /** * Get detailed error message * * @returns {string} Detailed error message */ getDetailedMessage(): string { let message = this.message; if (this.code) { message = `[${this.code}] ${message}`; } if (this.status) { message = `${message} (HTTP ${this.status})`; } return message; } } /** * Mask sensitive values in error details * * @param value - Value to mask * @returns Masked value */ function maskSensitiveValue(value: unknown): unknown { if (typeof value === "string") { // Mask potential passwords, tokens, keys 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)}****`; } // Mask long strings that might contain sensitive data if (value.length > 50) { return `${value.substring(0, 20)}...[${value.length - 40} chars]...${value.substring(value.length - 20)}`; } } return value; } /** * Get error code from message content (helper function) * * @param message - Error message * @returns Inferred ErrorCode */ function getErrorCodeFromMessage(message: string): ErrorCode | undefined { const lowerMessage = message.toLowerCase(); // Authentication errors 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"; } // Resource errors if (lowerMessage.includes("not found") || lowerMessage.includes("does not exist")) { return "NOT_FOUND"; } if (lowerMessage.includes("already exists") || lowerMessage.includes("duplicate")) { return "CONFLICT"; } // Validation errors if (lowerMessage.includes("validation") || lowerMessage.includes("invalid") || lowerMessage.includes("required") || lowerMessage.includes("missing")) { return "VALIDATION_ERROR"; } // Network errors if (lowerMessage.includes("network") || lowerMessage.includes("connection") || lowerMessage.includes("timeout")) { return "NETWORK_ERROR"; } if (lowerMessage.includes("rate limit")) { return "RATE_LIMIT_EXCEEDED"; } // Server errors 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"; } // Default to undefined - let caller decide return undefined; }