@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
text/typescript
/**
* 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;
}