GreenBean/greenbean.js
2025-02-17 21:46:57 +03:00

275 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;