feat(core): Реализация автоматических связей через соглашения
• Добавлена автоматическая детекция типов связей • Реализован Proxy-доступ к связям через свойства • Поддержка каскадного удаления данных • Добавлены транзакции с откатом состояния • Обновлена документация и примеры использования Миграция: - Вместо методов xown()/shared() используйте прямое присваивание - Методы getXown() заменены на доступ через свойства
This commit is contained in:
parent
f5c3712b18
commit
be9be042b8
649
README.md
649
README.md
@ -1,555 +1,242 @@
|
||||
# GreenBean
|
||||
# GreenBean Документация
|
||||
|
||||
GreenBean - это легковесная JavaScript библиотека для работы с данными, вдохновленная RedBeanPHP. Она предоставляет простой и гибкий способ хранения данных в JSON-формате с поддержкой связей между объектами.
|
||||
## 🌱 Введение
|
||||
**GreenBean** — это современная ORM для Node.js, сочетающая простоту RedBeanPHP с мощью JavaScript.
|
||||
Работает поверх JSON-хранилища, идеально подходит для прототипов, MVP и приложений с гибкой схемой данных.
|
||||
|
||||
## 📖 Содержание
|
||||
### Ключевые концепции:
|
||||
- **Динамическая схема** — не требует предварительного описания моделей
|
||||
- **Живые объекты** — изменения сохраняются автоматически
|
||||
- **Соглашения вместо конфигурации** — автоматическое разрешение связей
|
||||
- **Транзакционный подход** — атомарные операции изменения данных
|
||||
|
||||
- [Введение](#введение)
|
||||
- [Установка](#установка)
|
||||
- [Концепции](#концепции)
|
||||
- [Быстрый старт](#быстрый-старт)
|
||||
- [Основные операции](#основные-операции)
|
||||
- [Работа со связями](#работа-со-связями)
|
||||
- [Продвинутые возможности](#продвинутые-возможности)
|
||||
- [Лучшие практики](#лучшие-практики)
|
||||
- [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';
|
||||
npm install greenbean uuid
|
||||
```
|
||||
|
||||
Убедитесь, что в вашем `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');
|
||||
// 1. Инициализация
|
||||
const gb = new GreenBean('blog.json', {
|
||||
autoSave: true // Автосохранение при изменениях
|
||||
});
|
||||
|
||||
// База данных создастся автоматически
|
||||
// 2. Создание сущностей
|
||||
const author = await gb.dispense('Author', {
|
||||
name: 'Анна',
|
||||
bio: 'JS разработчик из Москвы'
|
||||
});
|
||||
|
||||
const post = await gb.dispense('Post', {
|
||||
title: 'Мой первый пост',
|
||||
content: '...'
|
||||
});
|
||||
|
||||
// 3. Установка связей
|
||||
post.author = author; // Один-ко-многим
|
||||
post.tags = [ // Многие-ко-многим
|
||||
await gb.dispense('Tag', {name: 'программирование'}),
|
||||
await gb.dispense('Tag', {name: 'учебник'})
|
||||
];
|
||||
|
||||
// 4. Добавление комментариев (Один-ко-многим)
|
||||
post.comments = [
|
||||
await gb.dispense('Comment', {text: 'Отличный пост!'}),
|
||||
await gb.dispense('Comment', {text: 'Спасибо за статью'})
|
||||
];
|
||||
|
||||
// 5. Получение связанных данных
|
||||
const loadedPost = await gb.load('Post', post.id);
|
||||
console.log('Автор:', (await loadedPost.author).name);
|
||||
console.log('Теги:', await loadedPost.tags);
|
||||
console.log('Комментарии:', await loadedPost.comments);
|
||||
```
|
||||
|
||||
### Пример простого блога
|
||||
## 🔄 Жизненный цикл бина
|
||||
1. **Создание** — `dispense()`
|
||||
2. **Модификация** — прямое изменение свойств
|
||||
3. **Сохранение** — автоматическое или через `store()`
|
||||
4. **Удаление** — `trash()`
|
||||
|
||||
```javascript
|
||||
// Создаем автора
|
||||
const author = await gb.dispense('author');
|
||||
author.name = 'Иван Петров';
|
||||
author.email = 'ivan@example.com';
|
||||
await gb.store(author);
|
||||
// Пример жизненного цикла
|
||||
const user = await gb.dispense('User'); // 1. Создание
|
||||
user.name = 'Иван'; // 2. Модификация
|
||||
await gb.store(user); // 3. Сохранение
|
||||
await gb.trash(user); // 4. Удаление
|
||||
```
|
||||
|
||||
// Создаем пост
|
||||
const post = await gb.dispense('post');
|
||||
post.title = 'Мой первый пост';
|
||||
post.content = 'Привет, мир!';
|
||||
post.created = new Date().toISOString();
|
||||
await gb.store(post);
|
||||
## 🤝 Работа со связями
|
||||
### Правила именования
|
||||
| Тип связи | Паттерн | Пример |
|
||||
|-----------------|---------------------------|---------------------|
|
||||
| Один-ко-многим | `parent.children` | `user.posts` |
|
||||
| Многие-ко-многим | `parent_relation` таблица | `post_tags` |
|
||||
| Один-к-одному | `parent.prop` + уникальность | `user.profile` |
|
||||
|
||||
// Связываем автора и пост
|
||||
await gb.xown(post, 'author', author);
|
||||
### Практические примеры
|
||||
#### 1. Социальная сеть
|
||||
```javascript
|
||||
// Пользователь и друзья (многие-ко-многим)
|
||||
const user = await gb.dispense('User');
|
||||
user.friends = [
|
||||
await gb.dispense('User', {name: 'Мария'}),
|
||||
await gb.dispense('User', {name: 'Петр'})
|
||||
];
|
||||
|
||||
// Создаем комментарии
|
||||
const comment1 = await gb.dispense('comment');
|
||||
comment1.text = 'Отличный пост!';
|
||||
comment1.author = 'Мария';
|
||||
await gb.store(comment1);
|
||||
// Группы (один-ко-многим)
|
||||
const group = await gb.dispense('Group');
|
||||
group.members = [
|
||||
await gb.dispense('User', {name: 'Анна'}),
|
||||
await gb.dispense('User', {name: 'Сергей'})
|
||||
];
|
||||
```
|
||||
|
||||
const comment2 = await gb.dispense('comment');
|
||||
comment2.text = 'Спасибо за статью';
|
||||
comment2.author = 'Петр';
|
||||
await gb.store(comment2);
|
||||
#### 2. Электронная коммерция
|
||||
```javascript
|
||||
// Заказ и товары (многие-ко-многим)
|
||||
const order = await gb.dispense('Order');
|
||||
order.products = [
|
||||
await gb.dispense('Product', {name: 'Ноутбук'}),
|
||||
await gb.dispense('Product', {name: 'Чехол'})
|
||||
];
|
||||
|
||||
// Связываем пост и комментарии
|
||||
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}`);
|
||||
// Платежная информация (один-к-одному)
|
||||
order.payment = await gb.dispense('Payment', {
|
||||
amount: 999.99,
|
||||
status: 'completed'
|
||||
});
|
||||
```
|
||||
|
||||
## Основные операции
|
||||
|
||||
### Создание объектов
|
||||
|
||||
## 🔍 Расширенный поиск
|
||||
### Поддерживаемые операторы
|
||||
```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']
|
||||
const results = await gb.find('User', {
|
||||
age: { gt: 18, lt: 30 }, // Больше 18 и меньше 30
|
||||
name: { like: 'Ив%' }, // Начинается с "Ив"
|
||||
email: { neq: null }, // Email не null
|
||||
rating: { between: [4, 5] } // Рейтинг от 4 до 5
|
||||
});
|
||||
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');
|
||||
const posts = await gb.find('Post',
|
||||
{ status: 'published' },
|
||||
{
|
||||
limit: 10,
|
||||
offset: 20,
|
||||
order: { created: 'DESC' }
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Обновление объектов
|
||||
|
||||
```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 product = await gb.dispense('Product');
|
||||
product.stock -= 1;
|
||||
|
||||
// Создаем элементы заказа
|
||||
const item1 = await gb.dispense('orderItem');
|
||||
item1.name = 'Товар 1';
|
||||
item1.price = 600;
|
||||
await gb.store(item1);
|
||||
const order = await gb.dispense('Order');
|
||||
order.items = [product];
|
||||
|
||||
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('Что-то пошло не так');
|
||||
if (product.stock < 0) {
|
||||
throw new Error('Нет в наличии');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Транзакция отменена:', error.message);
|
||||
console.error('Ошибка транзакции:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
### Заморозка данных
|
||||
|
||||
Заморозка предотвращает случайные изменения данных:
|
||||
|
||||
## 🧊 Заморозка данных
|
||||
```javascript
|
||||
// Замораживаем базу данных
|
||||
// Защита от изменений
|
||||
gb.freeze();
|
||||
|
||||
try {
|
||||
const user = await gb.dispense('user');
|
||||
user.name = 'Иван';
|
||||
await gb.store(user); // Выбросит ошибку
|
||||
} catch (error) {
|
||||
console.error('Нельзя изменять замороженные данные');
|
||||
const user = await gb.dispense('User'); // Ошибка!
|
||||
} catch (e) {
|
||||
console.error('База заморожена');
|
||||
}
|
||||
|
||||
// Размораживаем для внесения изменений
|
||||
// Временная разморозка
|
||||
gb.freeze(false);
|
||||
const tempUser = await gb.dispense('User');
|
||||
gb.freeze(true);
|
||||
```
|
||||
|
||||
### События
|
||||
|
||||
GreenBean позволяет отслеживать различные события:
|
||||
|
||||
## 🧩 Лучшие практики
|
||||
### 1. Сервисные классы
|
||||
```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);
|
||||
async register(userData) {
|
||||
const user = await this.gb.dispense('User', userData);
|
||||
user.verificationToken = crypto.randomBytes(32).toString('hex');
|
||||
return this.gb.store(user);
|
||||
}
|
||||
}
|
||||
|
||||
// Использование
|
||||
const userService = new UserService(gb);
|
||||
const user = await userService.create({
|
||||
name: 'Иван',
|
||||
email: 'ivan@example.com'
|
||||
});
|
||||
```
|
||||
|
||||
### Обработка ошибок
|
||||
|
||||
### 2. Валидация данных
|
||||
```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) {
|
||||
function validatePost(post) {
|
||||
const errors = [];
|
||||
|
||||
if (!user.name) {
|
||||
errors.push('Имя обязательно');
|
||||
if (!post.title || post.title.length < 5) {
|
||||
errors.push('Название слишком короткое');
|
||||
}
|
||||
|
||||
if (!user.email?.includes('@')) {
|
||||
errors.push('Некорректный email');
|
||||
if (!post.content || post.content.length < 100) {
|
||||
errors.push('Минимум 100 символов в содержании');
|
||||
}
|
||||
|
||||
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);
|
||||
### 3. Обработка ошибок
|
||||
```javascript
|
||||
async function safeDeleteUser(userId) {
|
||||
try {
|
||||
const user = await gb.load('User', userId);
|
||||
await gb.trash(user);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Ошибка удаления пользователя ${userId}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
## 📚 Полное API
|
||||
### Основные методы
|
||||
| Метод | Параметры | Возвращает | Описание |
|
||||
|-----------------------|----------------------|--------------------|--------------------------|
|
||||
| `dispense(type, props)` | Тип, свойства | Бин | Создает новый объект |
|
||||
| `store(bean)` | Бин | ID | Сохраняет изменения |
|
||||
| `load(type, id)` | Тип, ID | Бин/null | Загружает по ID |
|
||||
| `find(type, criteria)` | Тип, критерии | Массив бинов | Поиск с фильтрацией |
|
||||
| `trash(bean)` | Бин | void | Удаляет объект и связи |
|
||||
|
||||
#### `dispense(type)`
|
||||
Создает новый объект указанного типа.
|
||||
### Настройки
|
||||
```javascript
|
||||
new GreenBean('data.json', {
|
||||
autoSave: false, // Автоматическое сохранение
|
||||
freeze: false, // Начальное состояние блокировки
|
||||
cacheTTL: 300000 // Время жизни кеша (5 минут)
|
||||
});
|
||||
```
|
||||
|
||||
#### `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
|
||||
## 📜 Лицензия
|
||||
MIT License © 2025 Rockzo development
|
||||
```
|
||||
|
||||
506
greenbean.js
506
greenbean.js
@ -3,263 +3,89 @@ import { EventEmitter } from 'events';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class GreenBean extends EventEmitter {
|
||||
constructor(dbPath = 'data.json') {
|
||||
constructor(dbPath = 'data.json', { autoSave = false } = {}) {
|
||||
super();
|
||||
this.dbPath = dbPath;
|
||||
this.data = null;
|
||||
this.initialized = false;
|
||||
this.frozen = false;
|
||||
this.autoSave = autoSave;
|
||||
this._initPromise = null;
|
||||
this._transactionLock = false;
|
||||
this._relationCache = new WeakMap();
|
||||
}
|
||||
|
||||
async dispense(type) {
|
||||
if (!this.initialized) await this.init();
|
||||
|
||||
async dispense(type, props = {}) {
|
||||
await this.init();
|
||||
const bean = {
|
||||
id: uuidv4(),
|
||||
__type: type,
|
||||
__meta: {
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString()
|
||||
}
|
||||
},
|
||||
...props
|
||||
};
|
||||
|
||||
return new Proxy(bean, this.createHandler(type));
|
||||
return this.createProxy(bean);
|
||||
}
|
||||
|
||||
async load(type, id) {
|
||||
if (!this.initialized) await this.init();
|
||||
|
||||
const item = this.data[type]?.find(item => item.id === id);
|
||||
await this.init();
|
||||
const item = this.data[type]?.find(i => i.id === id);
|
||||
if (!item) return null;
|
||||
|
||||
return new Proxy({...item}, this.createHandler(type));
|
||||
return this.createProxy({...item});
|
||||
}
|
||||
|
||||
async store(bean) {
|
||||
if (this.frozen) throw new Error('Cannot modify frozen beans');
|
||||
if (!this.initialized) await this.init();
|
||||
if (this.frozen) throw new Error('Frozen database');
|
||||
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) {
|
||||
if (!this.data[type]) this.data[type] = [];
|
||||
|
||||
const index = this.data[type].findIndex(i => i.id === bean.id);
|
||||
if (index >= 0) {
|
||||
this.data[type][index] = {...bean};
|
||||
} else {
|
||||
this.data[type].push({...bean});
|
||||
}
|
||||
|
||||
await this.save();
|
||||
return bean.id;
|
||||
await this.autoSave && this.save();
|
||||
return bean;
|
||||
}
|
||||
|
||||
async trash(bean) {
|
||||
if (this.frozen) throw new Error('Cannot modify frozen beans');
|
||||
if (!this.initialized) await this.init();
|
||||
if (this.frozen) throw new Error('Frozen database');
|
||||
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;
|
||||
this.data[type] = this.data[type].filter(i => i.id !== bean.id);
|
||||
await this.handleCascadeDelete(bean);
|
||||
await this.autoSave && this.save();
|
||||
}
|
||||
|
||||
async find(type, criteria = {}) {
|
||||
if (!this.initialized) await this.init();
|
||||
|
||||
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)));
|
||||
.filter(item => this.matchCriteria(item, criteria))
|
||||
.map(item => this.createProxy({...item}));
|
||||
}
|
||||
|
||||
async findOne(type, criteria = {}) {
|
||||
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) {
|
||||
if (this._transactionLock) throw new Error('Transaction in progress');
|
||||
this._transactionLock = true;
|
||||
|
||||
const snapshot = JSON.stringify(this.data);
|
||||
try {
|
||||
const result = await callback();
|
||||
@ -268,8 +94,274 @@ class GreenBean extends EventEmitter {
|
||||
} catch (error) {
|
||||
this.data = JSON.parse(snapshot);
|
||||
throw error;
|
||||
} finally {
|
||||
this._transactionLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
createProxy(bean) {
|
||||
const handler = {
|
||||
get: (target, prop) => {
|
||||
if (prop === 'then') return; // Для async/await
|
||||
if (prop in target) return target[prop];
|
||||
|
||||
return new Proxy({}, {
|
||||
get: async (_, propName) => {
|
||||
const relations = await this.resolveRelations(target);
|
||||
return relations[prop];
|
||||
},
|
||||
apply: async () => {
|
||||
return this.handleRelationAccess(target, prop);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
set: async (target, prop, value) => {
|
||||
if (prop === '__type' || prop === '__meta') {
|
||||
throw new Error(`Cannot modify ${prop}`);
|
||||
}
|
||||
|
||||
const relationType = await this.detectRelationship(target.__type, prop);
|
||||
if (relationType) {
|
||||
await this.handleRelationUpdate(target, prop, value, relationType);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (prop.endsWith('List') && Array.isArray(value)) {
|
||||
target[prop] = value.map(i => ({
|
||||
id: i.id,
|
||||
__type: i.__type || this.guessType(i)
|
||||
}));
|
||||
} else {
|
||||
target[prop] = value;
|
||||
}
|
||||
|
||||
target.__meta.modified = new Date().toISOString();
|
||||
await this.autoSave && this.store(target);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return new Proxy(bean, handler);
|
||||
}
|
||||
|
||||
// ========== Relationship Handling ==========
|
||||
async resolveRelations(target) {
|
||||
if (!this._relationCache.has(target)) {
|
||||
this._relationCache.set(target, {});
|
||||
}
|
||||
const cache = this._relationCache.get(target);
|
||||
|
||||
return new Proxy({}, {
|
||||
get: (_, prop) => {
|
||||
if (!cache[prop]) {
|
||||
cache[prop] = this.handleRelationAccess(target, prop);
|
||||
}
|
||||
return cache[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async handleRelationAccess(target, prop) {
|
||||
const relation = await this.detectRelationship(target.__type, prop);
|
||||
|
||||
switch (relation.type) {
|
||||
case 'manyToMany':
|
||||
return this.getShared(target, prop);
|
||||
case 'oneToMany':
|
||||
return this.getOwn(target, prop);
|
||||
case 'oneToOne':
|
||||
return this.getXOwn(target, prop);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async handleRelationUpdate(target, prop, value, relation) {
|
||||
switch (relation.type) {
|
||||
case 'manyToMany':
|
||||
await this.shared(target, prop, value);
|
||||
break;
|
||||
case 'oneToMany':
|
||||
await this.own(target, prop, value);
|
||||
break;
|
||||
case 'oneToOne':
|
||||
await this.xownOne(target, prop, value);
|
||||
break;
|
||||
}
|
||||
this._relationCache.delete(target);
|
||||
}
|
||||
|
||||
async detectRelationship(parentType, property) {
|
||||
// Many-to-Many detection
|
||||
const linkTable = `${parentType}_${property}`;
|
||||
if (this.data[linkTable]) {
|
||||
return { type: 'manyToMany', linkTable };
|
||||
}
|
||||
|
||||
// One-to-Many detection
|
||||
const childType = property;
|
||||
if (this.data[childType]?.some(i => i[`${parentType}_id`])) {
|
||||
return { type: 'oneToMany', childType };
|
||||
}
|
||||
|
||||
// One-to-One detection
|
||||
if (this.data[property]?.some(i =>
|
||||
i[`${parentType}_id`] && this.isFieldUnique(property, `${parentType}_id`)
|
||||
)) {
|
||||
return { type: 'oneToOne', childType: property };
|
||||
}
|
||||
|
||||
return { type: 'none' };
|
||||
}
|
||||
|
||||
async getShared(target, prop) {
|
||||
const linkTable = `${target.__type}_${prop}`;
|
||||
const links = await this.find(linkTable, {
|
||||
[`${target.__type}_id`]: target.id
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
links.map(link =>
|
||||
this.load(link[`${prop}_type`], link[`${prop}_id`])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async getOwn(target, prop) {
|
||||
return this.find(prop, {
|
||||
[`${target.__type}_id`]: target.id
|
||||
});
|
||||
}
|
||||
|
||||
async getXOwn(target, prop) {
|
||||
return this.findOne(prop, {
|
||||
[`${target.__type}_id`]: target.id
|
||||
});
|
||||
}
|
||||
|
||||
async shared(target, prop, values) {
|
||||
const linkTable = `${target.__type}_${prop}`;
|
||||
await this.trashRelations(linkTable, target.id);
|
||||
|
||||
for (const value of [values].flat()) {
|
||||
const link = await this.dispense(linkTable, {
|
||||
[`${target.__type}_id`]: target.id,
|
||||
[`${prop}_id`]: value.id,
|
||||
[`${prop}_type`]: value.__type
|
||||
});
|
||||
await this.store(link);
|
||||
}
|
||||
}
|
||||
|
||||
async own(target, prop, values) {
|
||||
const children = [values].flat();
|
||||
for (const child of children) {
|
||||
child[`${target.__type}_id`] = target.id;
|
||||
await this.store(child);
|
||||
}
|
||||
}
|
||||
|
||||
async xownOne(target, prop, value) {
|
||||
const current = await this.getXOwn(target, prop);
|
||||
if (current) {
|
||||
delete current[`${target.__type}_id`];
|
||||
await this.store(current);
|
||||
}
|
||||
if (value) {
|
||||
value[`${target.__type}_id`] = target.id;
|
||||
await this.store(value);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Helpers ==========
|
||||
async init() {
|
||||
if (this._initPromise) return this._initPromise;
|
||||
this._initPromise = (async () => {
|
||||
try {
|
||||
const exists = await fs.access(this.dbPath).then(() => true).catch(() => false);
|
||||
this.data = exists ?
|
||||
JSON.parse(await fs.readFile(this.dbPath, 'utf-8')) :
|
||||
{};
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
throw new Error(`Init failed: ${error.message}`);
|
||||
}
|
||||
})();
|
||||
return this._initPromise;
|
||||
}
|
||||
|
||||
async save() {
|
||||
await fs.writeFile(this.dbPath, JSON.stringify(this.data, null, 2));
|
||||
this.emit('save');
|
||||
}
|
||||
|
||||
async handleCascadeDelete(bean) {
|
||||
const relations = await this.detectAllRelationships(bean.__type);
|
||||
for (const rel of relations) {
|
||||
switch (rel.type) {
|
||||
case 'oneToMany':
|
||||
const children = await this.find(rel.childType, {
|
||||
[`${bean.__type}_id`]: bean.id
|
||||
});
|
||||
for (const child of children) {
|
||||
await this.trash(child);
|
||||
}
|
||||
break;
|
||||
case 'oneToOne':
|
||||
const linked = await this.findOne(rel.childType, {
|
||||
[`${bean.__type}_id`]: bean.id
|
||||
});
|
||||
if (linked) await this.trash(linked);
|
||||
break;
|
||||
case 'manyToMany':
|
||||
await this.trashRelations(rel.linkTable, bean.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async trashRelations(linkTable, sourceId) {
|
||||
if (!this.data[linkTable]) return;
|
||||
this.data[linkTable] = this.data[linkTable].filter(
|
||||
i => i[`${linkTable.split('_')[0]}_id`] !== sourceId
|
||||
);
|
||||
}
|
||||
|
||||
matchCriteria(item, criteria) {
|
||||
return Object.entries(criteria).every(([key, condition]) => {
|
||||
if (typeof condition === 'object' && condition !== null) {
|
||||
return Object.entries(condition).every(([op, value]) => {
|
||||
switch(op) {
|
||||
case 'gt': return item[key] > value;
|
||||
case 'lt': return item[key] < value;
|
||||
case 'like': return new RegExp(value).test(item[key]);
|
||||
default: return item[key] === condition;
|
||||
}
|
||||
});
|
||||
}
|
||||
return item[key] === condition;
|
||||
});
|
||||
}
|
||||
|
||||
isFieldUnique(type, field) {
|
||||
const values = this.data[type]?.map(i => i[field]);
|
||||
return new Set(values).size === values?.length;
|
||||
}
|
||||
|
||||
guessType(obj) {
|
||||
return obj.__type || Object.getPrototypeOf(obj)?.constructor?.name.toLowerCase();
|
||||
}
|
||||
|
||||
freeze(frozen = true) {
|
||||
this.frozen = frozen;
|
||||
}
|
||||
|
||||
async nuke() {
|
||||
if (this.frozen) throw new Error('Cannot nuke frozen DB');
|
||||
this.data = {};
|
||||
await this.save();
|
||||
}
|
||||
}
|
||||
|
||||
export default GreenBean;
|
||||
Loading…
Reference in New Issue
Block a user