/** * Persistent mock database for development/testing * Saves to JSON file so data persists across restarts */ import { nanoid } from "nanoid"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Use HuggingFace Spaces persistent storage if available (/data is persistent) // Otherwise use local data directory function getDbPath(): string { const hfPersistentPath = "/data/mock_db.json"; const localPath = path.resolve(__dirname, "../data/mock_db.json"); // Check if we're on HuggingFace Spaces const isHfSpaces = process.env.SPACE_ID || process.env.SPACE_AUTHOR_NAME; if (isHfSpaces) { console.log("[MockDB] Running on HuggingFace Spaces"); // Check if /data exists and is writable if (fs.existsSync("/data")) { try { fs.accessSync("/data", fs.constants.W_OK); console.log("[MockDB] Using persistent storage: /data/mock_db.json"); return hfPersistentPath; } catch (e) { console.log("[MockDB] /data exists but not writable, using /app/data"); console.log("[MockDB] NOTE: Data will NOT persist across restarts!"); console.log("[MockDB] Enable persistent storage in Space settings for data persistence."); } } else { console.log("[MockDB] /data directory not found, using /app/data"); console.log("[MockDB] NOTE: Data will NOT persist across restarts!"); } } return localPath; } const DB_FILE = getDbPath(); interface MockUser { id: number; openId: string; name: string | null; email: string | null; loginMethod: string | null; role: "user" | "admin"; createdAt: Date; updatedAt: Date; lastSignedIn: Date; } interface MockTrack { id: number; userId: number | null; title: string; artist: string | null; trackType: "ai_generated" | "training_reference"; fileKey: string; fileUrl: string; fileSize: number | null; mimeType: string | null; duration: number | null; status: "pending" | "processing" | "completed" | "failed"; errorMessage: string | null; createdAt: Date; updatedAt: Date; } interface MockStem { id: number; trackId: number; stemType: "vocals" | "drums" | "bass" | "other"; fileKey: string; fileUrl: string; duration: number | null; createdAt: Date; } interface MockFingerprint { id: number; stemId: number; algorithm: string; fingerprintData: string; version: string | null; createdAt: Date; } interface MockEmbedding { id: number; stemId: number; model: string; embeddingVector: number[]; dimension: number; createdAt: Date; } interface MockAttributionResult { id: number; aiTrackId: number; aiStemId: number | null; trainingTrackId: number; trainingStemId: number | null; method: string; score: number; confidence: number | null; metadata: Record | null; createdAt: Date; } interface MockProcessingJob { id: number; trackId: number; jobType: "stem_separation" | "fingerprinting" | "embedding" | "attribution"; status: "pending" | "running" | "completed" | "failed"; progress: number; errorMessage: string | null; resultData: Record | null; startedAt: Date | null; completedAt: Date | null; createdAt: Date; } // Persistent storage with JSON file backup class MockDatabase { private users = new Map(); private tracks = new Map(); private stems = new Map(); private fingerprints = new Map(); private embeddings = new Map(); private attributionResults = new Map(); private processingJobs = new Map(); private nextUserId = 1; private nextTrackId = 1; private nextStemId = 1; private nextFingerprintId = 1; private nextEmbeddingId = 1; private nextAttributionId = 1; private nextJobId = 1; constructor() { this.load(); } private load(): void { try { if (fs.existsSync(DB_FILE)) { const data = JSON.parse(fs.readFileSync(DB_FILE, 'utf-8')); // Restore maps with date parsing const parseDate = (obj: Record) => { for (const key of Object.keys(obj)) { if (typeof obj[key] === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(obj[key] as string)) { obj[key] = new Date(obj[key] as string); } } return obj; }; if (data.users) { for (const u of data.users) { this.users.set(u.id, parseDate(u) as MockUser); } } if (data.tracks) { for (const t of data.tracks) { this.tracks.set(t.id, parseDate(t) as MockTrack); } } if (data.stems) { for (const s of data.stems) { this.stems.set(s.id, parseDate(s) as MockStem); } } if (data.fingerprints) { for (const f of data.fingerprints) { this.fingerprints.set(f.id, parseDate(f) as MockFingerprint); } } if (data.embeddings) { for (const e of data.embeddings) { this.embeddings.set(e.id, parseDate(e) as MockEmbedding); } } if (data.attributionResults) { for (const a of data.attributionResults) { this.attributionResults.set(a.id, parseDate(a) as MockAttributionResult); } } if (data.processingJobs) { for (const j of data.processingJobs) { this.processingJobs.set(j.id, parseDate(j) as MockProcessingJob); } } // Restore next IDs this.nextUserId = data.nextUserId || 1; this.nextTrackId = data.nextTrackId || 1; this.nextStemId = data.nextStemId || 1; this.nextFingerprintId = data.nextFingerprintId || 1; this.nextEmbeddingId = data.nextEmbeddingId || 1; this.nextAttributionId = data.nextAttributionId || 1; this.nextJobId = data.nextJobId || 1; console.log(`[MockDB] Loaded ${this.tracks.size} tracks, ${this.users.size} users from ${DB_FILE}`); } } catch (error) { console.error('[MockDB] Failed to load database:', error); } } private save(): void { try { // Ensure directory exists const dir = path.dirname(DB_FILE); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const data = { users: Array.from(this.users.values()), tracks: Array.from(this.tracks.values()), stems: Array.from(this.stems.values()), fingerprints: Array.from(this.fingerprints.values()), embeddings: Array.from(this.embeddings.values()), attributionResults: Array.from(this.attributionResults.values()), processingJobs: Array.from(this.processingJobs.values()), nextUserId: this.nextUserId, nextTrackId: this.nextTrackId, nextStemId: this.nextStemId, nextFingerprintId: this.nextFingerprintId, nextEmbeddingId: this.nextEmbeddingId, nextAttributionId: this.nextAttributionId, nextJobId: this.nextJobId, }; fs.writeFileSync(DB_FILE, JSON.stringify(data, null, 2)); } catch (error) { console.error('[MockDB] Failed to save database:', error); } } // User operations upsertUser(user: Partial & { openId: string }): MockUser { // Find existing user by openId let existing: MockUser | undefined; for (const u of this.users.values()) { if (u.openId === user.openId) { existing = u; break; } } if (existing) { const updated = { ...existing, ...user, updatedAt: new Date(), lastSignedIn: user.lastSignedIn || new Date(), }; this.users.set(existing.id, updated); this.save(); return updated; } const newUser: MockUser = { id: this.nextUserId++, openId: user.openId, name: user.name || null, email: user.email || null, loginMethod: user.loginMethod || null, role: user.role || "user", createdAt: new Date(), updatedAt: new Date(), lastSignedIn: user.lastSignedIn || new Date(), }; this.users.set(newUser.id, newUser); this.save(); return newUser; } getUserByOpenId(openId: string): MockUser | undefined { for (const user of this.users.values()) { if (user.openId === openId) { return user; } } return undefined; } // Track operations createTrack(track: Omit): number { const id = this.nextTrackId++; const newTrack: MockTrack = { id, ...track, createdAt: new Date(), updatedAt: new Date(), }; this.tracks.set(id, newTrack); this.save(); return id; } getTrackById(id: number): MockTrack | undefined { return this.tracks.get(id); } getUserTracks(userId: number): MockTrack[] { return Array.from(this.tracks.values()) .filter(t => t.userId === userId) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); } updateTrackStatus(id: number, status: MockTrack["status"], errorMessage?: string): void { const track = this.tracks.get(id); if (track) { track.status = status; if (errorMessage !== undefined) track.errorMessage = errorMessage; track.updatedAt = new Date(); this.save(); } } getTrainingTracks(): MockTrack[] { return Array.from(this.tracks.values()) .filter(t => t.trackType === "training_reference"); } // Stem operations createStem(stem: Omit): number { const id = this.nextStemId++; const newStem: MockStem = { id, ...stem, createdAt: new Date(), }; this.stems.set(id, newStem); this.save(); return id; } getTrackStems(trackId: number): MockStem[] { return Array.from(this.stems.values()).filter(s => s.trackId === trackId); } // Fingerprint operations createFingerprint(fp: Omit): number { const id = this.nextFingerprintId++; const newFp: MockFingerprint = { id, ...fp, createdAt: new Date(), }; this.fingerprints.set(id, newFp); this.save(); return id; } // Embedding operations createEmbedding(emb: Omit): number { const id = this.nextEmbeddingId++; const newEmb: MockEmbedding = { id, ...emb, createdAt: new Date(), }; this.embeddings.set(id, newEmb); this.save(); return id; } // Attribution operations createAttributionResult(attr: Omit): number { const id = this.nextAttributionId++; const newAttr: MockAttributionResult = { id, ...attr, createdAt: new Date(), }; this.attributionResults.set(id, newAttr); this.save(); return id; } getTrackAttributions(aiTrackId: number): Array { return Array.from(this.attributionResults.values()) .filter(a => a.aiTrackId === aiTrackId) .map(a => ({ ...a, trainingTrack: this.tracks.get(a.trainingTrackId), })) .sort((a, b) => b.score - a.score); } clearTrackAttributions(aiTrackId: number): number { let count = 0; for (const [id, attr] of this.attributionResults.entries()) { if (attr.aiTrackId === aiTrackId) { this.attributionResults.delete(id); count++; } } if (count > 0) { this.save(); } return count; } // Processing job operations createProcessingJob(job: Omit): number { const id = this.nextJobId++; const newJob: MockProcessingJob = { id, ...job, createdAt: new Date(), }; this.processingJobs.set(id, newJob); this.save(); return id; } updateProcessingJob(id: number, updates: Partial): void { const job = this.processingJobs.get(id); if (job) { Object.assign(job, updates); this.save(); } } getTrackJobs(trackId: number): MockProcessingJob[] { return Array.from(this.processingJobs.values()) .filter(j => j.trackId === trackId) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); } } // Singleton instance export const mockDb = new MockDatabase(); // Export type-compatible functions for use by the app export const isMockDb = true;