Spaces:
Build error
Build error
| 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<string, unknown>; | |
| } | |
| export class JsonStore<T extends StoredEntity> { | |
| 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<void> { | |
| await fs.mkdir(this.directory, { recursive: true }); | |
| } | |
| private async acquireLock(filePath: string): Promise<() => Promise<void>> { | |
| const release = await properLockfile.lock(filePath, this.lockOptions); | |
| return release; | |
| } | |
| async create(entity: T): Promise<T> { | |
| 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<T | null> { | |
| 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<T>): Promise<T | null> { | |
| 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<boolean> { | |
| 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<T[]> { | |
| 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<string, unknown>)[key] !== value) { | |
| matches = false; | |
| break; | |
| } | |
| } | |
| if (matches) { | |
| entities.push(entity); | |
| } | |
| } | |
| entities.sort((a, b) => { | |
| const aVal = (a as Record<string, unknown>)[sortBy]; | |
| const bVal = (b as Record<string, unknown>)[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<string, unknown> = {}): Promise<number> { | |
| 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<string, unknown>)[key] !== value) { | |
| matches = false; | |
| break; | |
| } | |
| } | |
| if (matches) count++; | |
| } | |
| return count; | |
| } | |
| async exists(id: string): Promise<boolean> { | |
| try { | |
| await fs.access(this.getFilePath(id)); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| async findOne(filter: Record<string, unknown>): Promise<T | null> { | |
| const entities = await this.findAll({ filter, limit: 1 }); | |
| return entities[0] || null; | |
| } | |
| } |