UNPKG

@smartsamurai/krapi-sdk

Version:

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

544 lines (500 loc) 16.2 kB
/** * Unified KRAPI Client * * A unified client that can operate in two modes: * 1. Database Mode: Direct database access (for backend/server-side) * 2. HTTP Mode: API client for remote access (for frontend/external) * * This client automatically switches between modes based on configuration. * * @module krapi-client * @example * // Database mode * const client = new KrapiClient({ * mode: 'database', * database: { databaseConnection: dbConnection } * }); * * @example * // HTTP mode * const client = new KrapiClient({ * mode: 'http', * http: { baseUrl: 'https://api.example.com', apiKey: 'key' } * }); */ import axios, { type AxiosInstance, type InternalAxiosRequestConfig, type AxiosResponse, type AxiosError, } from "axios"; import { AdminService } from "./admin-service"; import { AuthService } from "./auth-service"; import { CollectionsSchemaManager } from "./collections-schema-manager"; import { CollectionsService } from "./collections-service"; import { CollectionsTypeManager } from "./collections-type-manager"; import { CollectionsTypeValidator } from "./collections-type-validator"; import { Logger, DatabaseSDKConfig, HttpSDKConfig, BaseClient } from "./core"; import { KrapiError } from "./core/krapi-error"; import { DatabaseHealthManager } from "./database-health"; import { EmailService } from "./email-service"; import { HealthService } from "./health-service"; import { ProjectsService } from "./projects-service"; import { SchemaGenerator } from "./schema-generator"; import { SQLiteSchemaInspector } from "./sqlite-schema-inspector"; import { StorageService } from "./storage-service"; import { SystemService } from "./system-service"; import { TestingService } from "./testing-service"; import { FieldType } from "./types"; import { UsersService } from "./users-service"; /** * Client Mode Type * * @typedef {"database" | "http"} ClientMode */ // Client mode type export type ClientMode = "database" | "http"; /** * KRAPI Client Configuration * * @interface KrapiClientConfig * @property {ClientMode} mode - Client mode ('database' or 'http') * @property {DatabaseSDKConfig} [database] - Database configuration (required for database mode) * @property {HttpSDKConfig} [http] - HTTP configuration (required for http mode) */ export interface KrapiClientConfig { mode: ClientMode; database?: DatabaseSDKConfig; http?: HttpSDKConfig; } // Base service interface for mode switching (currently unused) // interface ServiceProvider { // database?: DatabaseConnection; // httpClient?: Record<string, unknown>; // Will be axios instance for HTTP mode // logger: Logger; // } /** * Unified KRAPI Client * * Unified client that supports both database and HTTP modes. * Automatically initializes appropriate services based on mode. * * @class KrapiClient * @implements {BaseClient} * @example * // Database mode * const client = new KrapiClient({ * mode: 'database', * database: { databaseConnection: dbConnection, logger: console } * }); * * @example * // HTTP mode * const client = new KrapiClient({ * mode: 'http', * http: { baseUrl: 'https://api.example.com', apiKey: 'key' } * }); */ export class KrapiClient implements BaseClient { public readonly mode: ClientMode; // Database-specific services (only available in database mode) public database?: DatabaseHealthManager; public schemaGenerator?: SchemaGenerator; // Collections management public collections: { typeManager?: CollectionsTypeManager; typeValidator?: CollectionsTypeValidator; schemaManager?: CollectionsSchemaManager; service?: CollectionsService; schemaInspector?: SQLiteSchemaInspector; } = {}; // Core services (available in both modes) public admin?: AdminService; public auth?: AuthService; public email?: EmailService; public health?: HealthService; public projects?: ProjectsService; public storage?: StorageService; public users?: UsersService; public system?: SystemService; public testing?: TestingService; private config: KrapiClientConfig; private logger: Logger; private httpClient?: AxiosInstance; // Axios instance for HTTP mode /** * Create a new KrapiClient instance * * @param {KrapiClientConfig} config - Client configuration * @throws {Error} If configuration is invalid * * @example * const client = new KrapiClient({ * mode: 'database', * database: { databaseConnection: dbConnection } * }); */ constructor(config: KrapiClientConfig) { this.config = config; this.mode = config.mode; // Initialize logger this.logger = config.database?.logger || config.http?.logger || console; if (config.mode === "database" && config.database) { this.initializeDatabaseMode(config.database); } else if (config.mode === "http" && config.http) { this.initializeHttpMode(config.http); } else { throw KrapiError.validationError( `Invalid configuration for mode: ${config.mode}`, "mode" ); } } private initializeDatabaseMode(dbConfig: DatabaseSDKConfig) { // Validate database connection if ( !dbConfig.databaseConnection || typeof dbConfig.databaseConnection.query !== "function" ) { throw KrapiError.validationError( "Database mode requires a valid database connection with query method", "databaseConnection" ); } const db = dbConfig.databaseConnection; const logger = this.logger; // Initialize database-specific services this.database = new DatabaseHealthManager(db, logger as Console); this.schemaGenerator = new SchemaGenerator( { databaseConnection: db }, { defaultStringLength: 255, defaultDecimalPrecision: 10, defaultDecimalScale: 2, generateIndexes: true, generateConstraints: true, generateRelations: true, } ); // Initialize collections management this.collections = { typeManager: new CollectionsTypeManager(db, logger as Console), typeValidator: new CollectionsTypeValidator(db, logger as Console), schemaManager: new CollectionsSchemaManager(db, logger as Console), service: new CollectionsService(db, logger as Console), schemaInspector: new SQLiteSchemaInspector(db, logger as Console), }; // Initialize core services with database this.admin = new AdminService(db, logger); this.auth = new AuthService(db, logger); this.email = new EmailService(db, logger); this.health = new HealthService(db, logger); this.projects = new ProjectsService(db, logger); this.storage = new StorageService(db, logger); this.users = new UsersService(db, logger); this.system = new SystemService("", ""); // No HTTP needed in database mode this.testing = new TestingService(db, logger); } private async initializeHttpMode(httpConfig: HttpSDKConfig) { // Create axios instance this.httpClient = axios.create({ baseURL: httpConfig.baseUrl, timeout: httpConfig.timeout || 30000, headers: { "Content-Type": "application/json", }, }); // Add request interceptor for authentication this.httpClient.interceptors.request.use( (config: InternalAxiosRequestConfig) => { if (httpConfig.sessionToken) { config.headers.Authorization = `Bearer ${httpConfig.sessionToken}`; } else if (httpConfig.apiKey) { config.headers["X-API-Key"] = httpConfig.apiKey; } return config; } ); // Add response interceptor for error handling this.httpClient.interceptors.response.use( (response: AxiosResponse) => response, (error: AxiosError) => { // Enhanced error handling if (error.response) { const { status, data } = error.response; const errorMessage = (data as { error?: string; message?: string })?.error || (data as { message?: string })?.message || error.message; const enhancedError = { ...error, message: errorMessage, status, isApiError: true, originalError: error, // Add flag for auth errors to help frontend detect them isAuthError: status === 401 || (typeof errorMessage === "string" && (errorMessage.includes("expired") || errorMessage.includes("Invalid") || errorMessage.includes("Unauthorized") || errorMessage.includes("log in again"))), }; return Promise.reject(enhancedError); } return Promise.reject(error); } ); // HTTP-based services initialization // These would make HTTP calls instead of direct database calls this.logger.info( "HTTP mode initialized, but HTTP services not yet implemented" ); } // Database Mode Helper Methods async performHealthCheck() { if (this.mode !== "database" || !this.database) { throw KrapiError.badRequest( "Health check only available in database mode" ); } return this.database.healthCheck(); } async autoFixDatabase() { if (this.mode !== "database" || !this.database) { throw KrapiError.badRequest("Auto-fix only available in database mode"); } return this.database.autoFix(); } async validateSchema() { if (this.mode !== "database" || !this.database) { throw KrapiError.badRequest( "Schema validation only available in database mode" ); } return this.database.validateSchema(); } async migrate() { if (this.mode !== "database" || !this.database) { throw KrapiError.badRequest("Migration only available in database mode"); } return this.database.migrate(); } // Collections Management (Database Mode) async createCollection( _projectId: string, // Currently unused, will be used for project-scoped collections name: string, schema: { description?: string; fields: Array<{ name: string; type: string; required?: boolean; unique?: boolean; default?: unknown; validation?: Record<string, unknown>; }>; indexes?: Array<{ name: string; fields: string[]; unique?: boolean; }>; } ) { if (this.mode !== "database" || !this.collections.schemaManager) { throw KrapiError.badRequest( "Collection creation only available in database mode" ); } return this.collections.schemaManager.createCollection({ name, ...(schema.description && { description: schema.description }), fields: schema.fields.map((f) => ({ name: f.name, type: f.type as FieldType, required: f.required ?? false, unique: f.unique ?? false, indexed: false, default: f.default, ...(f.validation && { validation: f.validation as Record<string, unknown>, }), })), ...(schema.indexes && { indexes: schema.indexes }), }); } async getCollection(projectId: string, name: string) { if (this.mode !== "database" || !this.collections.service) { throw KrapiError.badRequest( "Collection retrieval only available in database mode" ); } // Use CollectionsService.getCollection which supports UUID and case-insensitive name lookup return await this.collections.service.getCollection(projectId, name); } async getProjectCollections(projectId: string) { if (this.mode !== "database" || !this.collections.schemaManager) { throw KrapiError.badRequest( "Collection listing only available in database mode" ); } const collections = await this.collections.schemaManager.getCollections(); return collections.filter((c) => c.project_id === projectId); } // Document Management (Database Mode) async createDocument( projectId: string, collectionName: string, documentData: { data: Record<string, unknown>; created_by?: string } ) { if (this.mode !== "database" || !this.collections.service) { throw KrapiError.badRequest( "Document creation only available in database mode" ); } return this.collections.service.createDocument( projectId, collectionName, documentData ); } async getDocument( projectId: string, collectionName: string, documentId: string ) { if (this.mode !== "database" || !this.collections.service) { throw KrapiError.badRequest( "Document retrieval only available in database mode" ); } return this.collections.service.getDocumentById( projectId, collectionName, documentId ); } async updateDocument( projectId: string, collectionName: string, documentId: string, updateData: { data: Record<string, unknown>; updated_by?: string } ) { if (this.mode !== "database" || !this.collections.service) { throw KrapiError.badRequest( "Document update only available in database mode" ); } return this.collections.service.updateDocument( projectId, collectionName, documentId, updateData ); } async deleteDocument( projectId: string, collectionName: string, documentId: string, deletedBy?: string ) { if (this.mode !== "database" || !this.collections.service) { throw KrapiError.badRequest( "Document deletion only available in database mode" ); } return this.collections.service.deleteDocument( projectId, collectionName, documentId, deletedBy ); } async getDocuments( projectId: string, collectionName: string, filter?: Record<string, unknown>, options?: { limit?: number; offset?: number; orderBy?: string; order?: "asc" | "desc"; } ) { if (this.mode !== "database" || !this.collections.service) { throw KrapiError.badRequest( "Document listing only available in database mode" ); } return this.collections.service.getDocuments( projectId, collectionName, filter, options ); } // HTTP Mode Helper Methods setSessionToken(token: string) { if (this.mode !== "http") { throw KrapiError.badRequest("Session tokens only available in HTTP mode"); } // Update the http config and axios interceptor if (this.config.http) { this.config.http.sessionToken = token; delete this.config.http.apiKey; } } setApiKey(key: string) { if (this.mode !== "http") { throw KrapiError.badRequest("API keys only available in HTTP mode"); } // Update the http config and axios interceptor if (this.config.http) { this.config.http.apiKey = key; delete this.config.http.sessionToken; } } clearAuth() { if (this.mode !== "http") { throw KrapiError.badRequest("Auth clearing only available in HTTP mode"); } if (this.config.http) { delete this.config.http.sessionToken; delete this.config.http.apiKey; } } // System Methods async getSystemInfo() { if (this.system) { return this.system.getSystemInfo(); } throw KrapiError.serviceUnavailable("System service not available"); } // Close and cleanup async close() { try { if ( this.mode === "database" && this.config.database?.databaseConnection.end ) { await this.config.database.databaseConnection.end(); } this.logger.info("KrapiClient closed successfully"); } catch (error) { this.logger.error("Error closing KrapiClient:", error); } } } // Convenience factory functions export function createDatabaseClient(config: DatabaseSDKConfig): KrapiClient { return new KrapiClient({ mode: "database", database: config, }); } export function createHttpClient(config: HttpSDKConfig): KrapiClient { return new KrapiClient({ mode: "http", http: config, }); }