diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b58b603
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/GreenBean.iml b/.idea/GreenBean.iml
new file mode 100644
index 0000000..24643cc
--- /dev/null
+++ b/.idea/GreenBean.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml
new file mode 100644
index 0000000..d23208f
--- /dev/null
+++ b/.idea/jsLibraryMappings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..5f410e8
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d9a3e50
--- /dev/null
+++ b/README.md
@@ -0,0 +1,555 @@
+# GreenBean
+
+GreenBean - это легковесная JavaScript библиотека для работы с данными, вдохновленная RedBeanPHP. Она предоставляет простой и гибкий способ хранения данных в JSON-формате с поддержкой связей между объектами.
+
+## 📖 Содержание
+
+- [Введение](#введение)
+- [Установка](#установка)
+- [Концепции](#концепции)
+- [Быстрый старт](#быстрый-старт)
+- [Основные операции](#основные-операции)
+- [Работа со связями](#работа-со-связями)
+- [Продвинутые возможности](#продвинутые-возможности)
+- [Лучшие практики](#лучшие-практики)
+- [API Reference](#api-reference)
+- [Лицензия](#лицензия)
+
+## Введение
+
+### Что такое GreenBean?
+
+GreenBean - это JavaScript библиотека, которая позволяет работать с данными в простом и гибком стиле. Вместо того чтобы заранее определять структуру данных (схему), GreenBean позволяет создавать и изменять объекты "на лету".
+
+### Для кого это?
+
+- 👨💻 Разработчикам, которые хотят быстро прототипировать приложения
+- 🎓 Начинающим, которым нужен простой способ работы с данными
+- 🚀 Проектам, где структура данных часто меняется
+- 🛠️ Небольшим и средним приложениям
+
+### Почему GreenBean?
+
+- **Простота**: Нет необходимости писать схемы и миграции
+- **Гибкость**: Структура данных может меняться в процессе разработки
+- **Удобство**: Автоматическое создание связей между объектами
+- **Надежность**: Поддержка транзакций и защита от одновременных изменений
+- **Производительность**: Данные хранятся в JSON-файле, что идеально для небольших проектов
+
+## Установка
+
+### Прямое подключение
+
+1. Скопируйте файл `greenbean.js` в ваш проект
+2. Установите зависимость uuid:
+```bash
+npm install uuid
+```
+3. Импортируйте GreenBean в ваш проект:
+```javascript
+import GreenBean from './greenbean.js';
+```
+
+Убедитесь, что в вашем `package.json` установлен тип модуля:
+```json
+{
+ "type": "module"
+}
+```
+
+## Концепции
+
+### Бины (Beans)
+
+В GreenBean все объекты называются "бинами". Бин - это простой JavaScript объект, который автоматически получает:
+- Уникальный идентификатор (`id`)
+- Тип (`__type`)
+- Метаданные (`__meta`)
+
+```javascript
+// Создание бина
+const user = await gb.dispense('user');
+
+// Бин автоматически получает следующую структуру:
+{
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ __type: "user",
+ __meta: {
+ created: "2025-02-17T18:44:08.000Z",
+ modified: "2025-02-17T18:44:08.000Z"
+ }
+}
+```
+
+### Типы бинов
+
+Тип бина - это просто строка, которая определяет "категорию" объекта. Например:
+- `user` для пользователей
+- `post` для постов
+- `comment` для комментариев
+
+```javascript
+// Создание бинов разных типов
+const user = await gb.dispense('user');
+const post = await gb.dispense('post');
+const comment = await gb.dispense('comment');
+```
+
+### Динамические свойства
+
+Вы можете добавлять любые свойства к бину в любое время:
+
+```javascript
+const user = await gb.dispense('user');
+
+// Добавляем свойства
+user.name = 'Иван';
+user.email = 'ivan@example.com';
+
+// Позже можем добавить новые свойства
+user.age = 25;
+user.city = 'Москва';
+
+await gb.store(user);
+```
+
+## Быстрый старт
+
+### Создание базы данных
+
+```javascript
+import GreenBean from 'greenbean';
+
+// Создаем экземпляр GreenBean
+const gb = new GreenBean('data.json');
+
+// База данных создастся автоматически
+```
+
+### Пример простого блога
+
+```javascript
+// Создаем автора
+const author = await gb.dispense('author');
+author.name = 'Иван Петров';
+author.email = 'ivan@example.com';
+await gb.store(author);
+
+// Создаем пост
+const post = await gb.dispense('post');
+post.title = 'Мой первый пост';
+post.content = 'Привет, мир!';
+post.created = new Date().toISOString();
+await gb.store(post);
+
+// Связываем автора и пост
+await gb.xown(post, 'author', author);
+
+// Создаем комментарии
+const comment1 = await gb.dispense('comment');
+comment1.text = 'Отличный пост!';
+comment1.author = 'Мария';
+await gb.store(comment1);
+
+const comment2 = await gb.dispense('comment');
+comment2.text = 'Спасибо за статью';
+comment2.author = 'Петр';
+await gb.store(comment2);
+
+// Связываем пост и комментарии
+await gb.xown(post, 'comments', [comment1, comment2]);
+
+// Получаем пост с автором и комментариями
+const loadedPost = await gb.load('post', post.id);
+const postAuthor = await gb.getXown(loadedPost, 'author');
+const comments = await gb.getXown(loadedPost, 'comments');
+
+console.log(`Пост: ${loadedPost.title}`);
+console.log(`Автор: ${postAuthor.name}`);
+console.log('Комментарии:');
+comments.forEach(comment => {
+ console.log(`- ${comment.author}: ${comment.text}`);
+});
+```
+
+## Основные операции
+
+### Создание объектов
+
+```javascript
+// Простое создание
+const user = await gb.dispense('user');
+user.name = 'Иван';
+await gb.store(user);
+
+// Создание с начальными данными
+const post = await gb.dispense('post');
+Object.assign(post, {
+ title: 'Заголовок',
+ content: 'Содержание',
+ tags: ['javascript', 'tutorial']
+});
+await gb.store(post);
+```
+
+### Поиск объектов
+
+```javascript
+// Поиск всех пользователей
+const allUsers = await gb.find('user');
+
+// Поиск по критерию
+const activeUsers = await gb.find('user', { status: 'active' });
+
+// Поиск одного объекта
+const ivan = await gb.findOne('user', { name: 'Иван' });
+
+// Загрузка по ID
+const user = await gb.load('user', 'some-uuid');
+```
+
+### Обновление объектов
+
+```javascript
+// Загружаем объект
+const user = await gb.load('user', userId);
+
+// Обновляем свойства
+user.name = 'Новое имя';
+user.age = 26;
+
+// Сохраняем изменения
+await gb.store(user);
+```
+
+### Удаление объектов
+
+```javascript
+// Удаление одного объекта
+await gb.trash(user);
+
+// Удаление нескольких объектов
+const inactiveUsers = await gb.find('user', { status: 'inactive' });
+for (const user of inactiveUsers) {
+ await gb.trash(user);
+}
+```
+
+## Работа со связями
+
+### Типы связей
+
+GreenBean поддерживает три типа связей:
+
+1. **Один-к-одному** (one-to-one)
+ - Один объект связан с другим объектом
+ - Пример: пользователь и профиль
+
+2. **Один-ко-многим** (one-to-many)
+ - Один объект связан с несколькими объектами
+ - Пример: пост и комментарии
+
+3. **Многие-ко-многим** (many-to-many)
+ - Объекты могут быть связаны с множеством других объектов
+ - Пример: пользователи и группы
+
+### Связь один-к-одному
+
+```javascript
+// Пример: пользователь и профиль
+const user = await gb.dispense('user');
+user.name = 'Иван';
+await gb.store(user);
+
+const profile = await gb.dispense('profile');
+profile.bio = 'Программист JavaScript';
+profile.avatar = 'avatar.jpg';
+await gb.store(profile);
+
+// Связываем пользователя и профиль
+await gb.xown(user, 'profile', profile);
+
+// Получаем профиль пользователя
+const userProfile = await gb.getXown(user, 'profile');
+console.log(userProfile.bio); // "Программист JavaScript"
+
+// Удаляем связь
+await gb.xown(user, 'profile', null);
+```
+
+### Связь один-ко-многим
+
+```javascript
+// Пример: категория и товары
+const category = await gb.dispense('category');
+category.name = 'Электроника';
+await gb.store(category);
+
+// Создаем несколько товаров
+const products = [];
+for (const name of ['Телефон', 'Ноутбук', 'Планшет']) {
+ const product = await gb.dispense('product');
+ product.name = name;
+ await gb.store(product);
+ products.push(product);
+}
+
+// Связываем категорию и товары
+await gb.xown(category, 'products', products);
+
+// Получаем все товары категории
+const categoryProducts = await gb.getXown(category, 'products');
+categoryProducts.forEach(product => {
+ console.log(`- ${product.name}`);
+});
+
+// Добавляем новый товар к существующим
+const newProduct = await gb.dispense('product');
+newProduct.name = 'Смарт-часы';
+await gb.store(newProduct);
+
+const updatedProducts = [...products, newProduct];
+await gb.xown(category, 'products', updatedProducts);
+```
+
+### Связь многие-ко-многим
+
+```javascript
+// Пример: студенты и курсы
+const student = await gb.dispense('student');
+student.name = 'Мария';
+await gb.store(student);
+
+// Создаем несколько курсов
+const courses = [];
+for (const name of ['JavaScript', 'Python', 'Web Design']) {
+ const course = await gb.dispense('course');
+ course.name = name;
+ await gb.store(course);
+ courses.push(course);
+}
+
+// Записываем студента на курсы
+await gb.shared(student, 'course', courses);
+
+// Получаем все курсы студента
+const studentCourses = await gb.getShared(student, 'course');
+console.log('Курсы студента:');
+studentCourses.forEach(course => {
+ console.log(`- ${course.name}`);
+});
+
+// Отписываем от одного курса
+const remainingCourses = courses.slice(1);
+await gb.shared(student, 'course', remainingCourses);
+```
+
+## Продвинутые возможности
+
+### Транзакции
+
+Транзакции позволяют выполнять несколько операций как единое целое:
+
+```javascript
+try {
+ await gb.transaction(async () => {
+ // Создаем заказ
+ const order = await gb.dispense('order');
+ order.total = 1000;
+ await gb.store(order);
+
+ // Создаем элементы заказа
+ const item1 = await gb.dispense('orderItem');
+ item1.name = 'Товар 1';
+ item1.price = 600;
+ await gb.store(item1);
+
+ const item2 = await gb.dispense('orderItem');
+ item2.name = 'Товар 2';
+ item2.price = 400;
+ await gb.store(item2);
+
+ // Связываем заказ и элементы
+ await gb.xown(order, 'items', [item1, item2]);
+
+ // Если произойдет ошибка, все изменения будут отменены
+ throw new Error('Что-то пошло не так');
+ });
+} catch (error) {
+ console.error('Транзакция отменена:', error.message);
+}
+```
+
+### Заморозка данных
+
+Заморозка предотвращает случайные изменения данных:
+
+```javascript
+// Замораживаем базу данных
+gb.freeze();
+
+try {
+ const user = await gb.dispense('user');
+ user.name = 'Иван';
+ await gb.store(user); // Выбросит ошибку
+} catch (error) {
+ console.error('Нельзя изменять замороженные данные');
+}
+
+// Размораживаем для внесения изменений
+gb.freeze(false);
+```
+
+### События
+
+GreenBean позволяет отслеживать различные события:
+
+```javascript
+// Подписываемся на событие сохранения
+gb.on('save', () => {
+ console.log('База данных сохранена');
+});
+
+// Можно использовать для логирования
+gb.on('save', () => {
+ const now = new Date().toISOString();
+ console.log(`[${now}] Данные сохранены`);
+});
+```
+
+## Лучшие практики
+
+### Организация кода
+
+```javascript
+// Создайте класс для работы с определенным типом объектов
+class UserService {
+ constructor(gb) {
+ this.gb = gb;
+ }
+
+ async create(userData) {
+ const user = await this.gb.dispense('user');
+ Object.assign(user, userData);
+ await this.gb.store(user);
+ return user;
+ }
+
+ async findByEmail(email) {
+ return await this.gb.findOne('user', { email });
+ }
+
+ async addToGroup(user, group) {
+ await this.gb.shared(user, 'group', group);
+ }
+}
+
+// Использование
+const userService = new UserService(gb);
+const user = await userService.create({
+ name: 'Иван',
+ email: 'ivan@example.com'
+});
+```
+
+### Обработка ошибок
+
+```javascript
+async function safeOperation(callback) {
+ try {
+ return await callback();
+ } catch (error) {
+ console.error('Ошибка:', error.message);
+ // Можно добавить логирование или обработку ошибок
+ throw error;
+ }
+}
+
+// Использование
+await safeOperation(async () => {
+ const user = await gb.dispense('user');
+ user.name = 'Иван';
+ await gb.store(user);
+});
+```
+
+### Валидация данных
+
+```javascript
+function validateUser(user) {
+ const errors = [];
+
+ if (!user.name) {
+ errors.push('Имя обязательно');
+ }
+
+ if (!user.email?.includes('@')) {
+ errors.push('Некорректный email');
+ }
+
+ return errors;
+}
+
+// Использование
+const user = await gb.dispense('user');
+user.name = 'Иван';
+user.email = 'invalid-email';
+
+const errors = validateUser(user);
+if (errors.length > 0) {
+ console.error('Ошибки валидации:', errors);
+} else {
+ await gb.store(user);
+}
+```
+
+## API Reference
+
+### Основные методы
+
+#### `dispense(type)`
+Создает новый объект указанного типа.
+
+#### `store(bean)`
+Сохраняет объект в базе данных.
+
+#### `load(type, id)`
+Загружает объект по типу и ID.
+
+#### `find(type, criteria)`
+Ищет объекты по критериям.
+
+#### `findOne(type, criteria)`
+Ищет один объект по критериям.
+
+#### `trash(bean)`
+Удаляет объект из базы данных.
+
+### Методы для работы со связями
+
+#### `xown(bean, property, related)`
+Устанавливает связь один-к-одному или один-ко-многим.
+
+#### `getXown(bean, property)`
+Получает связанные объекты.
+
+#### `shared(bean, property, related)`
+Устанавливает связь многие-ко-многим.
+
+#### `getShared(bean, property)`
+Получает объекты из связи многие-ко-многим.
+
+### Служебные методы
+
+#### `freeze(state = true)`
+Замораживает или размораживает базу данных.
+
+#### `transaction(callback)`
+Выполняет операции в транзакции.
+
+#### `nuke()`
+Очищает всю базу данных.
+
+## Лицензия
+
+MIT
diff --git a/greenbean.js b/greenbean.js
index e69de29..a289d6b 100644
--- a/greenbean.js
+++ b/greenbean.js
@@ -0,0 +1,275 @@
+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;
\ No newline at end of file
diff --git a/greenbean.js~ b/greenbean.js~
new file mode 100644
index 0000000..4f87a1d
--- /dev/null
+++ b/greenbean.js~
@@ -0,0 +1,198 @@
+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 R(type, id = null) {
+ if (!this.initialized) await this.init();
+
+ if (id === null) {
+ return this.dispense(type);
+ }
+ return this.load(type, id);
+ }
+
+ 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;
+ }
+
+ // Вспомогательные методы
+ 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;
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c11bb4d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,6 @@
+{
+ "type": "module",
+ "dependencies": {
+ "uuid": "^9.0.1"
+ }
+}