pre-1
This commit is contained in:
parent
64fe81246e
commit
42c4aa51cb
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
12
.idea/GreenBean.iml
Normal file
12
.idea/GreenBean.iml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/jsLibraryMappings.xml
Normal file
6
.idea/jsLibraryMappings.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptLibraryMappings">
|
||||||
|
<includedPredefinedLibrary name="Node.js Core" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/GreenBean.iml" filepath="$PROJECT_DIR$/.idea/GreenBean.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
555
README.md
Normal file
555
README.md
Normal file
@ -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
|
||||||
275
greenbean.js
275
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;
|
||||||
198
greenbean.js~
Normal file
198
greenbean.js~
Normal file
@ -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;
|
||||||
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user