From 42c4aa51cb5f18aa66106538c3a172201fe4eb84 Mon Sep 17 00:00:00 2001 From: Vufer Date: Mon, 17 Feb 2025 21:46:57 +0300 Subject: [PATCH] pre-1 --- .idea/.gitignore | 5 + .idea/GreenBean.iml | 12 + .idea/jsLibraryMappings.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + README.md | 555 ++++++++++++++++++++++++++++++++++++ greenbean.js | 275 ++++++++++++++++++ greenbean.js~ | 198 +++++++++++++ package.json | 6 + 9 files changed, 1071 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/GreenBean.iml create mode 100644 .idea/jsLibraryMappings.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 greenbean.js~ create mode 100644 package.json 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" + } +}