Chat-With-AI / src /core /storage /jsonStore.ts
NathMen12's picture
Upload 56 files
ef73937 verified
Raw
History Blame Contribute Delete
4.98 kB
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;
}
}