import { promises as fs } from 'fs'; import { createRequire } from 'module'; import path from 'path'; import { fileURLToPath } from 'url'; import properLockfile from 'proper-lockfile'; import { config } from '@config/index.js'; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); export interface StoredEntity { id: string; createdAt: string; updatedAt: string; } export interface QueryOptions { limit?: number; offset?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; filter?: Record; } export class JsonStore { private readonly directory: string; private readonly lockOptions = { retries: 5, retryWait: 100, realpath: false, }; constructor(entityName: string) { this.directory = path.join(config.DATA_DIR, entityName); } private getFilePath(id: string): string { return path.join(this.directory, `${id}.json`); } async initialize(): Promise { await fs.mkdir(this.directory, { recursive: true }); } private async acquireLock(filePath: string): Promise<() => Promise> { const release = await properLockfile.lock(filePath, this.lockOptions); return release; } async create(entity: T): Promise { const filePath = this.getFilePath(entity.id); const release = await this.acquireLock(filePath); try { await fs.writeFile(filePath, JSON.stringify(entity, null, 2), 'utf-8'); return entity; } finally { await release(); } } async findById(id: string): Promise { const filePath = this.getFilePath(id); try { const data = await fs.readFile(filePath, 'utf-8'); return JSON.parse(data) as T; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return null; } throw error; } } async update(id: string, updates: Partial): Promise { const filePath = this.getFilePath(id); const release = await this.acquireLock(filePath); try { const existing = await this.findById(id); if (!existing) return null; const updated: T = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt, updatedAt: new Date().toISOString(), } as T; await fs.writeFile(filePath, JSON.stringify(updated, null, 2), 'utf-8'); return updated; } finally { await release(); } } async delete(id: string): Promise { const filePath = this.getFilePath(id); const release = await this.acquireLock(filePath); try { await fs.unlink(filePath); return true; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return false; } throw error; } finally { await release(); } } async findAll(options: QueryOptions = {}): Promise { const { limit = 100, offset = 0, sortBy = 'createdAt', sortOrder = 'desc', filter = {}, } = options; const files = await fs.readdir(this.directory); const jsonFiles = files.filter(f => f.endsWith('.json')); const entities: T[] = []; for (const file of jsonFiles) { const data = await fs.readFile(path.join(this.directory, file), 'utf-8'); const entity = JSON.parse(data) as T; let matches = true; for (const [key, value] of Object.entries(filter)) { if ((entity as Record)[key] !== value) { matches = false; break; } } if (matches) { entities.push(entity); } } entities.sort((a, b) => { const aVal = (a as Record)[sortBy]; const bVal = (b as Record)[sortBy]; if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1; if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1; return 0; }); return entities.slice(offset, offset + limit); } async count(filter: Record = {}): Promise { const files = await fs.readdir(this.directory); let count = 0; for (const file of files) { if (!file.endsWith('.json')) continue; const data = await fs.readFile(path.join(this.directory, file), 'utf-8'); const entity = JSON.parse(data) as T; let matches = true; for (const [key, value] of Object.entries(filter)) { if ((entity as Record)[key] !== value) { matches = false; break; } } if (matches) count++; } return count; } async exists(id: string): Promise { try { await fs.access(this.getFilePath(id)); return true; } catch { return false; } } async findOne(filter: Record): Promise { const entities = await this.findAll({ filter, limit: 1 }); return entities[0] || null; } }