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