import fs from 'fs/promises'; import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; class GreenBean extends EventEmitter { constructor(dbPath = 'data.json') { super(); this.dbPath = dbPath; this.data = null; this.initialized = false; this.frozen = false; } async dispense(type) { if (!this.initialized) await this.init(); const bean = { id: uuidv4(), __type: type, __meta: { created: new Date().toISOString(), modified: new Date().toISOString() } }; return new Proxy(bean, this.createHandler(type)); } async load(type, id) { if (!this.initialized) await this.init(); const item = this.data[type]?.find(item => item.id === id); if (!item) return null; return new Proxy({...item}, this.createHandler(type)); } async store(bean) { if (this.frozen) throw new Error('Cannot modify frozen beans'); if (!this.initialized) await this.init(); const type = bean.__type; if (!this.data[type]) this.data[type] = []; bean.__meta.modified = new Date().toISOString(); const index = this.data[type].findIndex(item => item.id === bean.id); if (index !== -1) { this.data[type][index] = {...bean}; } else { this.data[type].push({...bean}); } await this.save(); return bean.id; } async trash(bean) { if (this.frozen) throw new Error('Cannot modify frozen beans'); if (!this.initialized) await this.init(); const type = bean.__type; if (!this.data[type]) return; this.data[type] = this.data[type].filter(item => item.id !== bean.id); await this.save(); } // Работа со связями async own(bean, property, related) { if (!Array.isArray(related)) related = [related]; bean[property] = related.map(item => ({ id: item.id, __type: item.__type })); await this.store(bean); } async link(bean, linkType, attributes = {}) { const link = await this.dispense(linkType); Object.assign(link, attributes, { [`${bean.__type}Id`]: bean.id }); await this.store(link); return link; } async find(type, criteria = {}) { if (!this.initialized) await this.init(); if (!this.data[type]) return []; return this.data[type] .filter(item => { return Object.entries(criteria).every(([key, value]) => item[key] === value ); }) .map(item => new Proxy({...item}, this.createHandler(type))); } async findOne(type, criteria = {}) { const results = await this.find(type, criteria); return results[0] || null; } // Работа со связями в стиле RedBeanPHP async shared(bean, property, related) { if (!this.initialized) await this.init(); if (this.frozen) throw new Error('Cannot modify frozen beans'); const linkType = `${bean.__type}_${property}`; if (!Array.isArray(related)) related = [related]; // Удаляем старые связи const existingLinks = await this.find(linkType, { [`${bean.__type}Id`]: bean.id }); for (const link of existingLinks) { await this.trash(link); } // Создаем новые связи for (const item of related) { const link = await this.dispense(linkType); Object.assign(link, { [`${bean.__type}Id`]: bean.id, [`${item.__type}Id`]: item.id }); await this.store(link); } } async getShared(bean, property) { if (!this.initialized) await this.init(); const linkType = `${bean.__type}_${property}`; const links = await this.find(linkType, { [`${bean.__type}Id`]: bean.id }); const relatedType = property; const results = []; for (const link of links) { const relatedId = link[`${relatedType}Id`]; const related = await this.load(relatedType, relatedId); if (related) results.push(related); } return results; } async xownOne(bean, property, related) { if (!this.initialized) await this.init(); if (this.frozen) throw new Error('Cannot modify frozen beans'); if (related === null) { delete bean[property]; } else { bean[property] = { id: related.id, __type: related.__type }; } await this.store(bean); } async xown(bean, property, related) { if (!Array.isArray(related)) { return this.xownOne(bean, property, related); } return this.own(bean, property, related); } async getXown(bean, property) { if (!this.initialized) await this.init(); const ref = bean[property]; if (!ref) return null; if (Array.isArray(ref)) { const results = []; for (const item of ref) { const related = await this.load(item.__type, item.id); if (related) results.push(related); } return results; } return await this.load(ref.__type, ref.id); } // Вспомогательные методы async init() { try { const exists = await fs.access(this.dbPath) .then(() => true) .catch(() => false); if (exists) { const content = await fs.readFile(this.dbPath, 'utf-8'); this.data = JSON.parse(content); } else { this.data = {}; } this.initialized = true; } catch (error) { throw new Error(`Failed to initialize database: ${error.message}`); } } async save() { try { const tempPath = `${this.dbPath}.tmp`; await fs.writeFile(tempPath, JSON.stringify(this.data, null, 2)); await fs.rename(tempPath, this.dbPath); this.emit('save'); } catch (error) { throw new Error(`Failed to save database: ${error.message}`); } } createHandler(type) { return { get: (target, prop) => { if (prop.endsWith('List') && !target[prop]) { target[prop] = []; } return target[prop]; }, set: (target, prop, value) => { if (this.frozen) throw new Error('Cannot modify frozen beans'); if (prop.endsWith('List') && Array.isArray(value)) { target[prop] = value.map(item => ({ id: item.id, __type: item.__type || type })); } else { target[prop] = value; } return true; } }; } // Утилиты freeze(frozen = true) { this.frozen = frozen; } async nuke() { if (this.frozen) throw new Error('Cannot nuke frozen database'); this.data = {}; await this.save(); } async transaction(callback) { const snapshot = JSON.stringify(this.data); try { const result = await callback(); await this.save(); return result; } catch (error) { this.data = JSON.parse(snapshot); throw error; } } } export default GreenBean;