@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
1,441 lines (1,366 loc) • 46.1 kB
text/typescript
/**
* KRAPI Backend SDK
*
* Backend SDK for direct database access and backend operations.
* This provides real functionality for database health, schema management,
* collections operations, and auto-fixing capabilities.
*
* Designed for server-side use with direct database connections.
* All operations use the provided database connection for maximum performance.
*
* @class BackendSDK
* @example
* const sdk = new BackendSDK({
* databaseConnection: dbConnection,
* logger: console
* });
* const projects = await sdk.projects.getAllProjects();
*/
import { ActivityLogger } from "./activity-logger";
import { AdminService } from "./admin-service";
import { AuthService } from "./auth-service";
import { BackupService } from "./backup-service";
import { ChangelogService } from "./changelog-service";
import { CollectionsSchemaManager } from "./collections-schema-manager";
import {
CollectionsService,
CreateDocumentRequest,
DocumentQueryOptions,
} from "./collections-service";
import { CollectionsTypeManager } from "./collections-type-manager";
import { CollectionsTypeValidator } from "./collections-type-validator";
import { DatabaseSDKConfig, Logger } from "./core";
import { KrapiError } from "./core/krapi-error";
import { DatabaseHealthManager } from "./database-health";
import { EmailService } from "./email-service";
import { HealthService } from "./health-service";
import { MetadataManager } from "./metadata-manager";
import { PerformanceMonitor } from "./performance-monitor";
import { ProjectsService } from "./projects-service";
import { SchemaGenerator } from "./schema-generator";
import { BackupBackend } from "./services/backup/backup-backend.interface";
import { SQLiteSchemaInspector } from "./sqlite-schema-inspector";
import { StorageService } from "./storage-service";
import { SystemService } from "./system-service";
import { TestingService } from "./testing-service";
import {
FieldType,
FieldValidation,
Collection,
CollectionTypeDefinition,
Document,
} from "./types";
import { UsersService } from "./users-service";
export interface BackendSDKConfig extends DatabaseSDKConfig {
currentUserId?: string;
/** Optional backup backend implementation (defaults to FileBackupBackend) */
backupBackend?: BackupBackend;
/** Backup directory path (for FileBackupBackend, default: data/backups) */
backupsDir?: string;
/** Data directory path (default: data) */
dataPath?: string;
}
export class BackendSDK {
public database: DatabaseHealthManager;
public schemaGenerator: SchemaGenerator;
public system: SystemService;
public collections: {
typeManager: CollectionsTypeManager;
typeValidator: CollectionsTypeValidator;
schemaManager: CollectionsSchemaManager;
service: CollectionsService;
schemaInspector: SQLiteSchemaInspector;
};
public admin: AdminService;
public auth: AuthService;
public changelog: ChangelogService;
public email: EmailService;
public health: HealthService;
public projects: ProjectsService;
public storage: StorageService;
public users: UsersService;
public testing: TestingService;
public activity: ActivityLogger;
public backup: BackupService;
public metadata: MetadataManager;
public performance: PerformanceMonitor;
public apiKeys: {
getAll(
projectId: string,
options?: Record<string, unknown>
): Promise<Record<string, unknown>[]>;
get(
projectId: string,
keyId: string
): Promise<Record<string, unknown> | null>;
create(
projectId: string,
keyData: {
name: string;
description?: string;
scopes: string[];
expires_at?: string;
created_by?: string;
}
): Promise<Record<string, unknown>>;
update(
projectId: string,
keyId: string,
updates: Record<string, unknown>
): Promise<Record<string, unknown>>;
delete(projectId: string, keyId: string): Promise<{ success: boolean }>;
regenerate(
projectId: string,
keyId: string
): Promise<Record<string, unknown>>;
validateKey(apiKey: string): Promise<Record<string, unknown>>;
};
private config: BackendSDKConfig;
private logger: Logger;
/**
* Set the current user ID for operations that require user context
*
* Sets the user ID that will be used for operations requiring user context
* (e.g., created_by, updated_by fields).
*
* @param {string} userId - User ID to set as current user
* @returns {void}
*
* @example
* sdk.setCurrentUserId('user-id');
* // All subsequent operations will use this user ID for user context
*/
setCurrentUserId(userId: string): void {
this.config.currentUserId = userId;
}
/**
* Create a new BackendSDK instance
*
* Initializes all services with the provided database connection.
*
* @param {BackendSDKConfig} config - SDK configuration
* @param {DatabaseConnection} config.databaseConnection - Database connection (required)
* @param {Logger} [config.logger] - Logger instance (default: console)
* @param {boolean} [config.enableAutoFix] - Enable automatic schema fixes
* @param {boolean} [config.enableHealthChecks] - Enable health checks
* @param {number} [config.maxRetries] - Maximum retry attempts
* @param {string} [config.currentUserId] - Current user ID for operations
* @throws {Error} If database connection is invalid
*
* @example
* const sdk = new BackendSDK({
* databaseConnection: dbConnection,
* logger: console,
* enableAutoFix: true
* });
*/
constructor(config: BackendSDKConfig) {
this.config = config;
this.logger = config.logger || console;
// Validate database connection
if (
!config.databaseConnection ||
typeof config.databaseConnection.query !== "function"
) {
throw KrapiError.validationError(
"BackendSDK requires a valid database connection with query method",
"databaseConnection"
);
}
// Initialize real database health manager
this.database = new DatabaseHealthManager(
config.databaseConnection,
this.logger as Console
);
// Initialize schema generator with real configuration
this.schemaGenerator = new SchemaGenerator(
{ databaseConnection: config.databaseConnection },
{
defaultStringLength: 255,
defaultDecimalPrecision: 10,
defaultDecimalScale: 2,
generateIndexes: true,
generateConstraints: true,
generateRelations: true,
}
);
// Initialize system service
this.system = new SystemService("", ""); // Backend SDK doesn't need HTTP endpoints
// Initialize collections management system
// Use SQLite schema inspector since we're using SQLite
this.collections = {
typeManager: new CollectionsTypeManager(
config.databaseConnection,
this.logger as Console
),
typeValidator: new CollectionsTypeValidator(
config.databaseConnection,
this.logger as Console
),
schemaManager: new CollectionsSchemaManager(
config.databaseConnection,
this.logger as Console
),
service: new CollectionsService(
config.databaseConnection,
this.logger as Console
),
schemaInspector: new SQLiteSchemaInspector(
config.databaseConnection,
this.logger as Console
),
};
// Initialize admin service
this.admin = new AdminService(config.databaseConnection, this.logger);
// Initialize auth service
this.auth = new AuthService(config.databaseConnection, this.logger);
// Initialize email service
this.email = new EmailService(config.databaseConnection, this.logger);
// Initialize API keys functionality using admin service
this.apiKeys = {
getAll: async (projectId: string, _options?: Record<string, unknown>) => {
const result = await this.admin.getProjectApiKeys(projectId);
return (result as unknown as Record<string, unknown>[]) || [];
},
get: async (projectId: string, keyId: string) => {
const result = await this.admin.getProjectApiKey(keyId, projectId);
return (result as unknown as Record<string, unknown>) || null;
},
create: async (projectId: string, keyData: Record<string, unknown>) => {
const result = await this.admin.createProjectApiKey(
projectId,
keyData as {
name: string;
description?: string;
scopes: string[];
expires_at?: string;
created_by?: string;
}
);
return result as unknown as Record<string, unknown>;
},
update: async (
projectId: string,
keyId: string,
updates: Record<string, unknown>
) => {
const result = await this.admin.updateProjectApiKey(
keyId,
projectId,
updates
);
return result as unknown as Record<string, unknown>;
},
delete: async (projectId: string, keyId: string) => {
const success = await this.admin.deleteProjectApiKey(keyId, projectId);
return { success };
},
regenerate: async (projectId: string, keyId: string) => {
const result = await this.admin.regenerateProjectApiKey(
keyId,
projectId
);
return result as unknown as Record<string, unknown>;
},
validateKey: async (apiKey: string) => {
try {
const result = await config.databaseConnection.query(
`SELECT id, name, type, scopes, project_ids
FROM api_keys
WHERE key = $1 AND is_active = true`,
[apiKey]
);
if (result.rows.length === 0) {
return { valid: false };
}
const keyData = result.rows[0] as Record<string, unknown>;
return {
valid: true,
key_info: {
id: keyData.id,
name: keyData.name,
type: keyData.type,
scopes: keyData.scopes || [],
project_id: Array.isArray(keyData.project_ids) && keyData.project_ids.length > 0 ? keyData.project_ids[0] : undefined,
},
};
} catch {
return { valid: false };
}
},
};
// Initialize health service
this.health = new HealthService(config.databaseConnection, this.logger);
// Initialize projects service
this.projects = new ProjectsService(config.databaseConnection, this.logger);
// Initialize storage service
this.storage = new StorageService(config.databaseConnection, this.logger);
// Initialize users service
this.users = new UsersService(config.databaseConnection, this.logger);
// Initialize testing service
this.testing = new TestingService(config.databaseConnection, this.logger);
// Initialize changelog service
this.changelog = new ChangelogService(
config.databaseConnection,
this.logger
);
// Initialize activity logger
this.activity = new ActivityLogger(config.databaseConnection, this.logger);
// Initialize backup service
this.backup = new BackupService(
config.databaseConnection,
this.logger,
config.backupBackend,
config.backupsDir,
config.dataPath
);
// Set backup service in admin service
this.admin.setBackupService(this.backup);
// Initialize metadata manager
this.metadata = new MetadataManager(config.databaseConnection, this.logger);
// Initialize performance monitor
this.performance = new PerformanceMonitor(
config.databaseConnection,
this.logger
);
}
/**
* Perform database health check
*
* Runs comprehensive health checks on the database including schema validation,
* connection status, and data integrity checks.
*
* @returns {Promise<Record<string, unknown>>} Health check results
*
* @example
* const health = await sdk.performHealthCheck();
* console.log(health.status); // 'healthy' | 'degraded' | 'unhealthy'
*/
async performHealthCheck() {
return this.database.healthCheck();
}
/**
* Get project activity logs
*
* Retrieves activity logs for a specific project including changes to collections,
* documents, and other project entities.
*
* @param {string} projectId - Project ID
* @param {Object} [options] - Query options
* @param {number} [options.limit] - Maximum number of logs to return
* @param {number} [options.days] - Number of days to look back
* @returns {Promise<unknown[]>} Array of activity log entries
*
* @example
* const activity = await sdk.getProjectActivity('project-id', { limit: 50, days: 7 });
*/
async getProjectActivity(
projectId: string,
options?: { limit?: number; days?: number }
) {
return await this.changelog.getByEntity("project", projectId, options);
}
/**
* Automatically fix database schema issues
*
* Runs database health manager to detect and fix schema misalignments, missing fields,
* type mismatches, and other database health issues.
*
* @returns {Promise<Record<string, unknown>>} Auto-fix results
*
* @example
* const results = await sdk.autoFixDatabase();
* console.log(results.fixed); // Number of issues fixed
*/
async autoFixDatabase() {
return this.database.autoFix();
}
/**
* Validate database schema
*
* Validates the current database schema against expected schema definitions.
*
* @returns {Promise<Record<string, unknown>>} Validation results
*
* @example
* const validation = await sdk.validateSchema();
* if (!validation.valid) {
* console.log(validation.errors);
* }
*/
async validateSchema() {
return this.database.validateSchema();
}
/**
* Run database migrations
*
* Executes pending database migrations to update schema to latest version.
*
* @returns {Promise<Record<string, unknown>>} Migration results
*
* @example
* const results = await sdk.migrate();
* console.log(results.applied); // Number of migrations applied
*/
async migrate() {
return this.database.migrate();
}
/**
* Create a new collection
*
* Creates a new collection with the specified schema, fields, and indexes.
*
* @param {string} projectId - Project ID
* @param {string} name - Collection name
* @param {Object} schema - Collection schema definition
* @param {string} [schema.description] - Collection description
* @param {Array} schema.fields - Collection fields
* @param {string} schema.fields[].name - Field name
* @param {string} schema.fields[].type - Field type
* @param {boolean} [schema.fields[].required] - Whether field is required
* @param {boolean} [schema.fields[].unique] - Whether field is unique
* @param {unknown} [schema.fields[].default] - Default value
* @param {Record<string, unknown>} [schema.fields[].validation] - Validation rules
* @param {Array} [schema.indexes] - Collection indexes
* @param {string} schema.indexes[].name - Index name
* @param {string[]} schema.indexes[].fields - Index fields
* @param {boolean} [schema.indexes[].unique] - Whether index is unique
* @param {string} [createdBy] - User ID who created the collection
* @returns {Promise<Collection>} Created collection
*
* @example
* const collection = await sdk.createCollection('project-id', 'tasks', {
* description: 'Task management',
* fields: [
* { name: 'title', type: 'string', required: true },
* { name: 'status', type: 'string', default: 'pending' }
* ],
* indexes: [{ name: 'idx_title', fields: ['title'] }]
* });
*/
async createCollection(
projectId: string,
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;
}>;
},
createdBy?: string
) {
return this.collections.schemaManager.createCollection(
{
name,
project_id: projectId,
...(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 }),
},
createdBy
);
}
/**
* Get a collection by name or ID
*
* Retrieves a collection by project ID and collection name (case-insensitive) or UUID.
* Supports both UUID lookup and case-insensitive name lookup.
*
* @param {string} projectId - Project ID
* @param {string} name - Collection name or UUID
* @returns {Promise<Collection | null>} Collection or null if not found
*
* @example
* const collection = await sdk.getCollection('project-id', 'tasks');
* const collectionById = await sdk.getCollection('project-id', 'uuid-here');
*/
async getCollection(projectId: string, name: string) {
return await this.collections.service.getCollection(projectId, name);
}
/**
* Update a collection schema
*
* Updates collection description, fields, or indexes.
*
* @param {string} projectId - Project ID
* @param {string} name - Collection name
* @param {Object} updates - Update data
* @param {string} [updates.description] - New description
* @param {Array} [updates.fields] - Updated fields
* @param {Array} [updates.indexes] - Updated indexes
* @returns {Promise<Collection>} Updated collection
* @throws {Error} If collection not found
*
* @example
* const updated = await sdk.updateCollection('project-id', 'tasks', {
* description: 'Updated description',
* fields: [{ name: 'priority', type: 'string' }]
* });
*/
async updateCollection(
projectId: string,
name: string,
updates: {
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;
}>;
}
) {
const collection = await this.getCollection(projectId, name);
if (!collection) {
throw KrapiError.notFound(`Collection '${name}' not found`, { projectId, name });
}
return this.collections.schemaManager.updateCollection(collection.id, {
...(updates.description && { description: updates.description }),
...(updates.fields && {
fields: updates.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>,
}),
})),
}),
...(updates.indexes && { indexes: updates.indexes }),
});
}
/**
* Delete a collection
*
* Deletes a collection and all its documents. This operation is irreversible.
*
* @param {string} projectId - Project ID
* @param {string} name - Collection name
* @returns {Promise<boolean>} True if deleted, false if not found
*
* @example
* const deleted = await sdk.deleteCollection('project-id', 'tasks');
*/
async deleteCollection(projectId: string, name: string) {
const collection = await this.getCollection(projectId, name);
if (!collection) {
return false;
}
return this.collections.schemaManager.deleteCollection(collection.id);
}
/**
* Get all collections for a project
*
* Retrieves all collections belonging to a specific project.
*
* @param {string} projectId - Project ID
* @returns {Promise<Collection[]>} Array of collections
*
* @example
* const collections = await sdk.getProjectCollections('project-id');
*/
async getProjectCollections(projectId: string) {
const collections = await this.collections.schemaManager.getCollections();
return collections.filter((c) => c.project_id === projectId);
}
/**
* Get project statistics
*
* Retrieves comprehensive statistics for a project including collection counts,
* document counts, storage usage, and activity metrics.
*
* @param {string} projectId - Project ID
* @returns {Promise<ProjectStats>} Project statistics
*
* @example
* const stats = await sdk.getProjectStatistics('project-id');
* console.log(stats.collection_count, stats.document_count);
*/
async getProjectStatistics(projectId: string) {
return this.projects.getProjectStatistics(projectId);
}
/**
* Create a document in a collection
*
* Creates a new document with the provided data in the specified collection.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {Record<string, unknown>} data - Document data
* @returns {Promise<Document>} Created document
*
* @example
* const doc = await sdk.createDocument('project-id', 'tasks', {
* title: 'New Task',
* status: 'pending'
* });
*/
async createDocument(
projectId: string,
collectionName: string,
data: Record<string, unknown>
) {
// Create document using CollectionsService
return this.collections.service.createDocument(projectId, collectionName, {
data,
created_by: this.config.currentUserId || "system", // Use current user ID or fallback to system
});
}
/**
* Create multiple documents in a collection
*
* Creates multiple documents in a single operation for better performance.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {CreateDocumentRequest[]} documents - Array of document data
* @returns {Promise<Document[]>} Array of created documents
*
* @example
* const docs = await sdk.createDocuments('project-id', 'tasks', [
* { data: { title: 'Task 1' } },
* { data: { title: 'Task 2' } }
* ]);
*/
async createDocuments(
projectId: string,
collectionName: string,
documents: CreateDocumentRequest[]
) {
// Create multiple documents using CollectionsService
// Documents are already in the correct format, just pass them through
return this.collections.service.createDocuments(
projectId,
collectionName,
documents
);
}
/**
* Get a document by ID
*
* Retrieves a specific document from a collection by its ID.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {string} documentId - Document ID
* @returns {Promise<Document | null>} Document or null if not found
*
* @example
* const doc = await sdk.getDocument('project-id', 'tasks', 'doc-id');
*/
async getDocument(
projectId: string,
collectionName: string,
documentId: string
) {
// Get document using CollectionsService
return this.collections.service.getDocumentById(
projectId,
collectionName,
documentId
);
}
/**
* Update a document
*
* Updates an existing document with new data.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {string} documentId - Document ID
* @param {Record<string, unknown>} data - Updated data
* @returns {Promise<Document>} Updated document
*
* @example
* const updated = await sdk.updateDocument('project-id', 'tasks', 'doc-id', {
* status: 'completed'
* });
*/
async updateDocument(
projectId: string,
collectionName: string,
documentId: string,
data: Record<string, unknown>
) {
// Update document using CollectionsService
return this.collections.service.updateDocument(
projectId,
collectionName,
documentId,
{
data,
updated_by: this.config.currentUserId || "system", // Use current user ID or fallback to system
}
);
}
/**
* Delete a document
*
* Deletes a document from a collection. This operation is irreversible.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {string} documentId - Document ID
* @returns {Promise<boolean>} True if deleted, false if not found
*
* @example
* const deleted = await sdk.deleteDocument('project-id', 'tasks', 'doc-id');
*/
async deleteDocument(
projectId: string,
collectionName: string,
documentId: string
) {
// Delete document using CollectionsService
return this.collections.service.deleteDocument(
projectId,
collectionName,
documentId
);
}
/**
* Get documents from a collection
*
* Retrieves documents from a collection with optional filtering, pagination, and sorting.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {Object} [filter] - Filter options
* @param {Record<string, unknown>} [filter.field_filters] - Field-based filters
* @param {string} [filter.search] - Search query
* @param {Object} [options] - Query options
* @param {number} [options.limit] - Maximum number of documents to return
* @param {number} [options.offset] - Number of documents to skip
* @param {string} [options.orderBy] - Field to sort by
* @param {string} [options.order] - Sort order ('asc' | 'desc')
* @returns {Promise<Document[]>} Array of documents
*
* @example
* const docs = await sdk.getDocuments('project-id', 'tasks', {
* field_filters: { status: 'pending' }
* }, { limit: 10, offset: 0, orderBy: 'created_at', order: 'desc' });
*/
async getDocuments(
projectId: string,
collectionName: string,
filter?: {
field_filters?: Record<string, unknown>;
search?: string;
},
options?: {
limit?: number;
offset?: number;
orderBy?: string;
order?: "asc" | "desc";
}
) {
// Convert options to CollectionsService format
const queryOptions: DocumentQueryOptions = {};
if (options?.limit !== undefined) {
queryOptions.limit = options.limit;
}
if (options?.offset !== undefined) {
queryOptions.offset = options.offset;
}
if (options?.orderBy !== undefined) {
queryOptions.sort_by = options.orderBy;
}
if (options?.order !== undefined) {
queryOptions.sort_order = options.order;
}
// Get documents using CollectionsService
return this.collections.service.getDocuments(
projectId,
collectionName,
filter,
queryOptions
);
}
/**
* Count documents in a collection
*
* Returns the total count of documents matching the specified filter.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {Object} [filter] - Filter options
* @param {Record<string, unknown>} [filter.where] - Field-based filters
* @param {string} [filter.search] - Search query
* @returns {Promise<number>} Document count
*
* @example
* const count = await sdk.countDocuments('project-id', 'tasks', {
* where: { status: 'pending' }
* });
*/
async countDocuments(
projectId: string,
collectionName: string,
filter?: {
where?: Record<string, unknown>;
search?: string;
}
) {
// Convert options to CollectionsService format
const documentFilter = filter?.where
? { field_filters: filter.where }
: filter?.search
? { search: filter.search }
: undefined;
// Get document count using CollectionsService
return this.collections.service.countDocuments(
projectId,
collectionName,
documentFilter
);
}
/**
* Update multiple documents
*
* Updates multiple documents in a single operation for better performance.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {Array} updates - Array of document updates
* @param {string} updates[].id - Document ID
* @param {Record<string, unknown>} updates[].data - Updated data
* @returns {Promise<Document[]>} Array of updated documents
*
* @example
* const updated = await sdk.updateDocuments('project-id', 'tasks', [
* { id: 'doc-1', data: { status: 'completed' } },
* { id: 'doc-2', data: { status: 'in-progress' } }
* ]);
*/
async updateDocuments(
projectId: string,
collectionName: string,
updates: Array<{
id: string;
data: Record<string, unknown>;
}>
) {
// Update documents using CollectionsService
return this.collections.service.updateDocuments(
projectId,
collectionName,
updates
);
}
/**
* Delete multiple documents
*
* Deletes multiple documents in a single operation. Returns detailed results
* including which documents were successfully deleted and any errors.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {string[]} documentIds - Array of document IDs to delete
* @returns {Promise<Object>} Deletion results
* @returns {boolean} returns.success - True if at least one document was deleted
* @returns {boolean[]} returns.results - Array of deletion results per document
* @returns {number} returns.deleted_count - Number of successfully deleted documents
* @returns {string[]} returns.errors - Array of document IDs that failed to delete
*
* @example
* const result = await sdk.deleteDocuments('project-id', 'tasks', ['doc-1', 'doc-2']);
* console.log(`Deleted ${result.deleted_count} documents`);
*/
async deleteDocuments(
projectId: string,
collectionName: string,
documentIds: string[]
): Promise<{
success: boolean;
results: boolean[];
deleted_count: number;
errors: string[];
}> {
const results = await this.collections.service.deleteDocuments(
projectId,
collectionName,
documentIds
);
const deletedCount = results.filter((result) => result === true).length;
const errors = results
.map((result, index) => (result === false ? documentIds[index] : null))
.filter((id) => id !== null) as string[];
return {
success: deletedCount > 0, // Success if at least one document was deleted
results,
deleted_count: deletedCount,
errors,
};
}
/**
* Search documents in a collection
*
* Performs full-text search on documents in a collection with optional field filtering.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {Object} query - Search query
* @param {string} [query.text] - Search text
* @param {string[]} [query.fields] - Fields to search in
* @param {Record<string, unknown>} [query.filters] - Additional filters
* @param {number} [query.limit] - Maximum number of results
* @param {number} [query.offset] - Number of results to skip
* @returns {Promise<Document[]>} Array of matching documents
*
* @example
* const results = await sdk.searchDocuments('project-id', 'tasks', {
* text: 'urgent',
* fields: ['title', 'description'],
* limit: 20
* });
*/
async searchDocuments(
projectId: string,
collectionName: string,
query: {
text?: string;
fields?: string[];
filters?: Record<string, unknown>;
limit?: number;
offset?: number;
}
): Promise<Document[]> {
const searchOptions: {
limit?: number;
offset?: number;
} = {};
if (query.limit !== undefined) {
searchOptions.limit = query.limit;
}
if (query.offset !== undefined) {
searchOptions.offset = query.offset;
}
return this.collections.service.searchDocuments(
projectId,
collectionName,
query.text || "",
searchOptions
);
}
/**
* Aggregate documents using a pipeline
*
* Performs aggregation operations on documents using a pipeline of operations.
*
* @param {string} projectId - Project ID
* @param {string} collectionName - Collection name
* @param {Array<Record<string, unknown>>} pipeline - Aggregation pipeline
* @returns {Promise<Array<Record<string, unknown>>>} Aggregated results
*
* @example
* const results = await sdk.aggregateDocuments('project-id', 'tasks', [
* { $group: { _id: '$status', count: { $sum: 1 } } }
* ]);
*/
async aggregateDocuments(
projectId: string,
collectionName: string,
pipeline: Array<Record<string, unknown>>
): Promise<Array<Record<string, unknown>>> {
return this.collections.service.aggregateDocuments(
projectId,
collectionName,
pipeline
);
}
/**
* Generate TypeScript types for project collections
*
* Generates comprehensive TypeScript type definitions for all collections
* in a project based on their schemas.
*
* @param {string} projectId - Project ID
* @returns {Promise<Array>} Array of type definitions for each collection
* @throws {Error} If no collections found for project
*
* @example
* const types = await sdk.generateTypes('project-id');
* types.forEach(t => console.log(t.typescriptTypes));
*/
async generateTypes(projectId: string) {
// Get project collections and generate types
const collections = await this.getProjectCollections(projectId);
if (collections.length === 0) {
throw KrapiError.notFound(`No collections found for project`, { projectId });
}
// Generate comprehensive TypeScript types for all collections
const typeDefinitions = await Promise.all(
collections.map(async (collection) => {
const collectionData =
await this.collections.schemaManager.getCollection(collection.id);
if (!collectionData) {
return {
collectionId: collection.id,
collectionName: collection.name,
fields: collection.fields,
typescriptTypes: "// Collection not found",
message: "Collection not found",
};
}
// Convert Collection to CollectionTypeDefinition format
const typeDefinition =
this.convertCollectionToTypeDefinition(collectionData);
const types =
await this.collections.typeManager.generateTypeScriptTypes(
typeDefinition
);
return {
collectionId: collection.id,
collectionName: collection.name,
fields: collection.fields,
typescriptTypes: types,
message: "Type generation completed successfully",
};
})
);
return typeDefinitions;
}
/**
* Validate TypeScript types for project collections
*
* Validates that all collections in a project have correct and consistent
* type definitions, checking for type mismatches and inconsistencies.
*
* @param {string} projectId - Project ID
* @returns {Promise<Array>} Array of validation results for each collection
* @throws {Error} If no collections found for project
*
* @example
* const results = await sdk.validateTypes('project-id');
* results.forEach(r => {
* if (!r.isValid) console.log(r.issues);
* });
*/
async validateTypes(projectId: string) {
// Get project collections and validate types
const collections = await this.getProjectCollections(projectId);
if (collections.length === 0) {
throw KrapiError.notFound(`No collections found for project`, { projectId });
}
// Validate types for all collections using the type validator
const validationResults = await Promise.all(
collections.map(async (collection) => {
const collectionData =
await this.collections.schemaManager.getCollection(collection.id);
if (!collectionData) {
return {
collectionId: collection.id,
collectionName: collection.name,
isValid: false,
issues: ["Collection not found"],
suggestions: ["Check collection ID"],
message: "Collection not found",
};
}
// Convert Collection to CollectionTypeDefinition format
const typeDefinition =
this.convertCollectionToTypeDefinition(collectionData);
const validation =
await this.collections.typeValidator.validateCollectionTypes(
typeDefinition
);
return {
collectionId: collection.id,
collectionName: collection.name,
isValid: validation.isValid,
issues: validation.issues,
suggestions: validation.suggestions,
message: validation.isValid
? "Type validation passed"
: "Type validation failed",
};
})
);
return validationResults;
}
/**
* Inspect database schema for a project
*
* Inspects and returns the actual database schema for all collections
* in a project, showing the real structure as stored in the database.
*
* @param {string} projectId - Project ID
* @returns {Promise<Array>} Array of schema inspections for each collection
* @throws {Error} If no collections found for project
*
* @example
* const schemas = await sdk.inspectSchema('project-id');
* schemas.forEach(s => console.log(s.table_name, s.columns));
*/
async inspectSchema(projectId: string) {
// Get project collections and inspect their schemas
const collections = await this.getProjectCollections(projectId);
if (collections.length === 0) {
throw KrapiError.notFound(`No collections found for project`, { projectId });
}
// Inspect schemas for all collections in the project
const inspections = await Promise.all(
collections.map(async (collection) => {
return this.collections.schemaInspector.getTableSchema(collection.name);
})
);
return inspections;
}
/**
* Get table information
*
* Retrieves detailed schema information for a specific table/collection.
*
* @param {string} projectId - Project ID (unused, kept for API consistency)
* @param {string} tableName - Table/collection name
* @returns {Promise<Record<string, unknown>>} Table schema information
*
* @example
* const info = await sdk.getTableInfo('project-id', 'tasks');
* console.log(info.columns, info.indexes);
*/
async getTableInfo(_projectId: string, tableName: string) {
// Get table info using the schema inspector
return this.collections.schemaInspector.getTableSchema(tableName);
}
/**
* Get system information
*
* Retrieves general system information including version, capabilities, and configuration.
*
* @returns {Promise<Record<string, unknown>>} System information
*
* @example
* const info = await sdk.getSystemInfo();
* console.log(info.version, info.features);
*/
async getSystemInfo() {
return this.system.getSystemInfo();
}
/**
* Get system status
*
* Retrieves current system status including health, uptime, and operational state.
*
* @returns {Promise<Record<string, unknown>>} System status
* @throws {Error} If not yet implemented
*
* @example
* const status = await sdk.getSystemStatus();
* console.log(status.health, status.uptime);
*/
async getSystemStatus() {
// This would need to be implemented in SystemService
throw KrapiError.serviceUnavailable("System status not yet implemented in SystemService");
}
/**
* Close the SDK and clean up resources
*
* Closes database connections and cleans up all resources.
* Should be called when the SDK is no longer needed.
*
* @returns {Promise<void>}
*
* @example
* await sdk.close();
*/
async close() {
try {
if (this.config.databaseConnection.end) {
await this.config.databaseConnection.end();
}
this.logger.info("BackendSDK closed successfully");
} catch (error) {
this.logger.error("Error closing BackendSDK:", error);
}
}
/**
* Convert Collection to CollectionTypeDefinition format
*/
private convertCollectionToTypeDefinition(
collection: Collection
): CollectionTypeDefinition {
const typeDef: CollectionTypeDefinition = {
id: collection.id,
name: collection.name,
version: "1.0.0",
schema: {
fields: collection.schema.fields.map((field) => {
const mappedField: {
name: string;
type: FieldType;
required: boolean;
unique: boolean;
indexed: boolean;
default?: unknown;
description?: string;
validation?: FieldValidation;
length?: number;
precision?: number;
scale?: number;
nullable?: boolean;
primary?: boolean;
sqlite_type?: FieldType;
typescript_type?: FieldType;
relation?: Record<string, unknown>;
} = {
name: field.name,
type: field.type,
required: field.required,
unique: field.unique,
indexed: field.indexed || false,
};
if (field.default_value !== undefined) {
mappedField.default = field.default_value;
}
if (field.description !== undefined) {
mappedField.description = field.description;
}
if (field.validation !== undefined) {
mappedField.validation = field.validation;
}
if (field.length !== undefined) {
mappedField.length = field.length;
}
if (field.precision !== undefined) {
mappedField.precision = field.precision;
}
if (field.scale !== undefined) {
mappedField.scale = field.scale;
}
if (field.nullable !== undefined) {
mappedField.nullable = field.nullable;
}
if (field.primary !== undefined) {
mappedField.primary = field.primary;
}
if (field.type !== undefined) {
mappedField.sqlite_type = field.type;
mappedField.typescript_type = field.type;
}
if (field.relation !== undefined) {
mappedField.relation = field.relation;
}
return mappedField;
}),
indexes: (collection.schema.indexes || []).map((index) => ({
name: index.name,
fields: index.fields,
unique: index.unique || false,
type: "btree" as const,
})),
constraints: [],
relations: [],
},
validation_rules: [],
auto_fix_rules: [],
created_at: collection.created_at,
updated_at: collection.updated_at,
created_by: "system",
project_id: collection.project_id,
fields: collection.schema.fields.map((field) => {
const mappedField: {
name: string;
type: FieldType;
required: boolean;
unique: boolean;
indexed: boolean;
default?: unknown;
description?: string;
validation?: FieldValidation;
length?: number;
precision?: number;
scale?: number;
nullable?: boolean;
primary?: boolean;
sqlite_type?: FieldType;
typescript_type?: FieldType;
relation?: Record<string, unknown>;
} = {
name: field.name,
type: field.type,
required: field.required,
unique: field.unique,
indexed: field.indexed || false,
};
if (field.default_value !== undefined) {
mappedField.default = field.default_value;
}
if (field.description !== undefined) {
mappedField.description = field.description;
}
if (field.validation !== undefined) {
mappedField.validation = field.validation;
}
if (field.length !== undefined) {
mappedField.length = field.length;
}
if (field.precision !== undefined) {
mappedField.precision = field.precision;
}
if (field.scale !== undefined) {
mappedField.scale = field.scale;
}
if (field.nullable !== undefined) {
mappedField.nullable = field.nullable;
}
if (field.primary !== undefined) {
mappedField.primary = field.primary;
}
if (field.type !== undefined) {
mappedField.sqlite_type = field.type;
mappedField.typescript_type = field.type;
}
if (field.relation !== undefined) {
mappedField.relation = field.relation;
}
return mappedField;
}),
indexes: (collection.schema.indexes || []).map((index) => ({
name: index.name,
fields: index.fields,
unique: index.unique || false,
type: "btree" as const,
})),
constraints: [],
relations: [],
};
if (collection.description !== undefined) {
typeDef.description = collection.description;
}
if (collection.metadata !== undefined) {
typeDef.metadata = collection.metadata;
}
return typeDef;
}
}