Skip to main content
Glama
HeptabaseDataService.ts8.91 kB
import fs from 'fs-extra'; import path from 'path'; import { HeptabaseBackupData, Whiteboard, Card, CardInstance, Connection, SearchQuery } from '@/types/heptabase'; export interface HeptabaseDataServiceConfig { dataPath: string; cacheEnabled: boolean; cacheTTL: number; } interface WhiteboardResult { whiteboard: Whiteboard; cards?: Card[]; connections?: Connection[]; } interface CardResult { card: Card; instances: CardInstance[]; } export class HeptabaseDataService { private config: HeptabaseDataServiceConfig; private data: HeptabaseBackupData = { whiteboards: {}, cards: {}, cardInstances: {}, connections: {} }; private cache: Map<string, { data: any; timestamp: number }> = new Map(); constructor(config: HeptabaseDataServiceConfig) { this.config = config; } async loadData(): Promise<void> { const { dataPath } = this.config; try { // Check for All-Data.json (actual Heptabase backup format) const allDataPath = path.join(dataPath, 'All-Data.json'); if (await fs.pathExists(allDataPath)) { const allData = await fs.readJson(allDataPath); // Load whiteboards from whiteBoardList if (allData.whiteBoardList) { this.data.whiteboards = this.arrayToObject(allData.whiteBoardList); } // Load cards from cardList if (allData.cardList) { this.data.cards = this.arrayToObject(allData.cardList); } // Load card instances from cardInstances if (allData.cardInstances) { this.data.cardInstances = this.arrayToObject(allData.cardInstances); } // Load connections from connections if (allData.connections) { this.data.connections = this.arrayToObject(allData.connections); } } else { // Fall back to test format with separate files // Load whiteboards const whiteboardPath = path.join(dataPath, 'whiteboard.json'); if (await fs.pathExists(whiteboardPath)) { const whiteboards = await fs.readJson(whiteboardPath); this.data.whiteboards = this.arrayToObject(whiteboards); } // Load cards const cardPath = path.join(dataPath, 'card.json'); if (await fs.pathExists(cardPath)) { const cards = await fs.readJson(cardPath); this.data.cards = this.arrayToObject(cards); } // Load card instances const instancePath = path.join(dataPath, 'card-Instance.json'); if (await fs.pathExists(instancePath)) { const instances = await fs.readJson(instancePath); this.data.cardInstances = this.arrayToObject(instances); } // Load connections const connectionPath = path.join(dataPath, 'connection.json'); if (await fs.pathExists(connectionPath)) { const connections = await fs.readJson(connectionPath); this.data.connections = this.arrayToObject(connections); } } } catch (error) { throw error; } } private arrayToObject<T extends { id: string }>(array: T[]): Record<string, T> { return array.reduce((obj, item) => { obj[item.id] = item; return obj; }, {} as Record<string, T>); } async searchWhiteboards(query: SearchQuery): Promise<Whiteboard[]> { const cacheKey = `whiteboards:${JSON.stringify(query)}`; if (this.config.cacheEnabled) { const cached = this.getFromCache(cacheKey); if (cached) return cached; } const results = Object.values(this.data.whiteboards).filter(whiteboard => { // Filter out trashed whiteboards if (whiteboard.isTrashed) return false; // Search by query if (query.query) { const searchTerm = query.query.toLowerCase(); if (!whiteboard.name.toLowerCase().includes(searchTerm)) { return false; } } // Filter by date range if (query.dateRange) { const createdDate = new Date(whiteboard.createdTime); if (createdDate < query.dateRange.start || createdDate > query.dateRange.end) { return false; } } return true; }); if (this.config.cacheEnabled) { this.setCache(cacheKey, results); } return results; } async searchCards(query: SearchQuery): Promise<Card[]> { const cacheKey = `cards:${JSON.stringify(query)}`; if (this.config.cacheEnabled) { const cached = this.getFromCache(cacheKey); if (cached) return cached; } const results = Object.values(this.data.cards).filter(card => { // Filter out trashed cards if (card.isTrashed) return false; // Search by query in title and content if (query.query) { const searchTerm = query.query.toLowerCase(); const titleMatch = card.title?.toLowerCase().includes(searchTerm) || false; const contentMatch = card.content.toLowerCase().includes(searchTerm); if (!titleMatch && !contentMatch) { return false; } } // Filter by whiteboard if (query.whiteboardId) { const hasInstance = Object.values(this.data.cardInstances).some( instance => instance.cardId === card.id && instance.whiteboardId === query.whiteboardId ); if (!hasInstance) return false; } // Filter by date range if (query.dateRange) { const createdDate = new Date(card.createdTime); if (createdDate < query.dateRange.start || createdDate > query.dateRange.end) { return false; } } return true; }); if (this.config.cacheEnabled) { this.setCache(cacheKey, results); } return results; } async getWhiteboard( whiteboardId: string, options: { includeCards?: boolean; includeConnections?: boolean } = {} ): Promise<WhiteboardResult> { const whiteboard = this.data.whiteboards[whiteboardId]; if (!whiteboard) { throw new Error('Whiteboard not found'); } const result: WhiteboardResult = { whiteboard }; if (options.includeCards) { const instances = Object.values(this.data.cardInstances).filter( instance => instance.whiteboardId === whiteboardId ); const cardIds = instances.map(instance => instance.cardId); result.cards = cardIds.map(id => this.data.cards[id]).filter(Boolean); } if (options.includeConnections) { result.connections = Object.values(this.data.connections).filter( connection => connection.whiteboardId === whiteboardId ); } return result; } async getCard(cardId: string): Promise<CardResult> { const card = this.data.cards[cardId]; if (!card) { throw new Error('Card not found'); } const instances = Object.values(this.data.cardInstances).filter( instance => instance.cardId === cardId ); return { card, instances }; } async getCardsByArea( whiteboardId: string, x: number, y: number, radius: number ): Promise<Card[]> { const instances = Object.values(this.data.cardInstances).filter(instance => { if (instance.whiteboardId !== whiteboardId) return false; const distance = Math.sqrt( Math.pow(instance.x - x, 2) + Math.pow(instance.y - y, 2) ); return distance <= radius; }); const cardIds = instances.map(instance => instance.cardId); return cardIds.map(id => this.data.cards[id]).filter(Boolean); } async getConnections(cardId?: string): Promise<Connection[]> { if (cardId) { // Find all instances of this card const instances = Object.values(this.data.cardInstances).filter( instance => instance.cardId === cardId ); const instanceIds = instances.map(instance => instance.id); // Find connections where this card's instances are involved return Object.values(this.data.connections).filter(connection => { return instanceIds.includes(connection.beginId) || instanceIds.includes(connection.endId); }); } return Object.values(this.data.connections); } async getWhiteboards(): Promise<Whiteboard[]> { return Object.values(this.data.whiteboards).filter(wb => !wb.isTrashed); } async getCards(): Promise<Card[]> { return Object.values(this.data.cards).filter(card => !card.isTrashed); } getData(): HeptabaseBackupData { return this.data; } private getFromCache(key: string): any { const cached = this.cache.get(key); if (!cached) return null; const now = Date.now(); if (now - cached.timestamp > this.config.cacheTTL * 1000) { this.cache.delete(key); return null; } return cached.data; } private setCache(key: string, data: any): void { this.cache.set(key, { data, timestamp: Date.now() }); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LarryStanley/heptabase-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server