Files
DLE/docs.ru/back-docs/tables-system.md

1642 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

[English](../../docs.en/back-docs/tables-system.md) | **Русский**
# Система электронных таблиц в DLE
> **Временный документ для внутреннего анализа**
---
## 📋 Содержание
1. [Обзор системы](#обзор-системы)
2. [Архитектура базы данных](#архитектура-базы-данных)
3. [Типы полей](#типы-полей)
4. [Функциональные возможности](#функциональные-возможности)
5. [Связи между таблицами](#связи-между-таблицами)
6. [Интеграция с AI (RAG)](#интеграция-с-ai-rag)
7. [API Reference](#api-reference)
8. [Примеры использования](#примеры-использования)
9. [Безопасность](#безопасность)
---
## Обзор системы
### Что это?
Система электронных таблиц в DLE — это **полнофункциональная база данных с графическим интерфейсом**, аналог **Notion Database** или **Airtable**, встроенный в приложение.
### Ключевые особенности
```
┌─────────────────────────────────────────────────────────┐
│ Электронные таблицы DLE │
├─────────────────────────────────────────────────────────┤
│ │
│ ✅ 6 типов полей (text, number, relation, lookup, etc.)│
│ ✅ Связи между таблицами (1:1, 1:N, N:N) │
│ ✅ Lookup и подстановка данных │
│ ✅ Фильтрация и сортировка │
│ ✅ Real-time обновления (WebSocket) │
│ ✅ Интеграция с AI (RAG для поиска) │
│ ✅ Шифрование всех данных (AES-256) │
│ ✅ Плейсхолдеры для API доступа │
│ ✅ Каскадное удаление │
│ ✅ Массовые операции │
│ │
└─────────────────────────────────────────────────────────┘
```
### Отличия от Excel/Google Sheets
| Функция | Excel/Sheets | DLE Tables |
|---------|--------------|------------|
| **Типизация данных** | Слабая | Строгая (6 типов) |
| **Связи между таблицами** | Нет | Да (relation, lookup) |
| **AI поиск** | Нет | Да (RAG, векторный поиск) |
| **Real-time обновления** | Частичная | Полная (WebSocket) |
| **Шифрование** | Нет | AES-256 для всех данных |
| **API доступ** | Ограниченный | Полный REST API |
| **Права доступа** | Базовые | Детальные (по ролям) |
---
## Архитектура базы данных
### Схема таблиц (PostgreSQL)
```sql
┌──────────────────────────────────────────────────────────┐
user_tables
├──────────────────────────────────────────────────────────┤
id SERIAL PRIMARY KEY
name_encrypted TEXT NOT NULL
description_encrypted TEXT
is_rag_source_id INTEGER (ссылка на is_rag_source)
created_at TIMESTAMP
updated_at TIMESTAMP
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
user_columns
├──────────────────────────────────────────────────────────┤
id SERIAL PRIMARY KEY
table_id INTEGER user_tables(id)
name_encrypted TEXT NOT NULL
type_encrypted TEXT NOT NULL
placeholder_encrypted TEXT (для API)
placeholder TEXT (незашифрованный)
options JSONB (настройки)
order INTEGER (порядок отображения)
created_at TIMESTAMP
updated_at TIMESTAMP
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
user_rows
├──────────────────────────────────────────────────────────┤
id SERIAL PRIMARY KEY
table_id INTEGER user_tables(id)
order INTEGER (порядок строк)
created_at TIMESTAMP
updated_at TIMESTAMP
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
user_cell_values
├──────────────────────────────────────────────────────────┤
id SERIAL PRIMARY KEY
row_id INTEGER user_rows(id)
column_id INTEGER user_columns(id)
value_encrypted TEXT (зашифрованное значение)
created_at TIMESTAMP
updated_at TIMESTAMP
UNIQUE(row_id, column_id) Одна ячейка = одно значение│
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
user_table_relations
├──────────────────────────────────────────────────────────┤
id SERIAL PRIMARY KEY
from_row_id INTEGER user_rows(id)
column_id INTEGER user_columns(id)
to_table_id INTEGER user_tables(id)
to_row_id INTEGER user_rows(id)
created_at TIMESTAMP
updated_at TIMESTAMP
└──────────────────────────────────────────────────────────┘
```
### Каскадное удаление
```
Удаление таблицы (user_tables)
├── Удаляет все столбцы (user_columns)
├── Удаляет все строки (user_rows)
│ └── Удаляет все значения ячеек (user_cell_values)
└── Удаляет все связи (user_table_relations)
```
**Важно**: Используется `ON DELETE CASCADE` для автоматической очистки.
### Индексы для производительности
```sql
-- Индексы на связях (user_table_relations)
CREATE INDEX idx_user_table_relations_from_row ON user_table_relations(from_row_id);
CREATE INDEX idx_user_table_relations_column ON user_table_relations(column_id);
CREATE INDEX idx_user_table_relations_to_table ON user_table_relations(to_table_id);
CREATE INDEX idx_user_table_relations_to_row ON user_table_relations(to_row_id);
```
**Эффект**: Быстрая фильтрация и поиск по связанным таблицам.
---
## Типы полей
### 1. Text (Текст)
**Описание**: Обычное текстовое поле
```json
{
"type": "text",
"options": null
}
```
**Использование**:
- Имена
- Описания
- Email
- URL
- Любой текст
### 2. Number (Число)
**Описание**: Числовое поле
```json
{
"type": "number",
"options": null
}
```
**Использование**:
- Цены
- Количество
- Рейтинги
- Проценты
### 3. Multiselect (Множественный выбор)
**Описание**: Выбор нескольких значений из списка
```json
{
"type": "multiselect",
"options": {
"choices": ["Вариант 1", "Вариант 2", "Вариант 3"]
}
}
```
**Использование**:
- Теги
- Категории
- Статусы
- Навыки
### 4. Multiselect-Relation (Мультивыбор из таблицы)
**Описание**: Множественный выбор строк из другой таблицы
```json
{
"type": "multiselect-relation",
"options": {
"relatedTableId": 5,
"relatedColumnId": 12
}
}
```
**Использование**:
- Связь Контактов → Теги (N:N)
- Связь Задач → Исполнители (N:N)
- Связь Продуктов → Категории (N:N)
**Хранение**: В таблице `user_table_relations` создается несколько записей:
```
from_row_id=100, column_id=3, to_table_id=5, to_row_id=20
from_row_id=100, column_id=3, to_table_id=5, to_row_id=21
from_row_id=100, column_id=3, to_table_id=5, to_row_id=22
```
### 5. Relation (Связь)
**Описание**: Связь с одной строкой из другой таблицы (1:1 или 1:N)
```json
{
"type": "relation",
"options": {
"relatedTableId": 3,
"relatedColumnId": 8
}
}
```
**Использование**:
- Задача → Проект (N:1)
- Контакт → Компания (N:1)
- Заказ → Клиент (N:1)
**Хранение**: В `user_table_relations` создается одна запись:
```
from_row_id=50, column_id=2, to_table_id=3, to_row_id=15
```
### 6. Lookup (Подстановка)
**Описание**: Автоматическая подстановка значения из связанной таблицы
```json
{
"type": "lookup",
"options": {
"relatedTableId": 4,
"relatedColumnId": 10,
"lookupColumnId": 11 // Какое поле подставлять
}
}
```
**Пример**:
```
Таблица "Заказы"
├── order_id (text)
├── product (relation → Продукты)
└── product_price (lookup → Продукты.price)
Когда выбираешь product, автоматически подставляется цена!
```
**Использование**:
- Цены из справочника
- Email из контактов
- Статусы из связанных задач
---
## Функциональные возможности
### 1. CRUD операции
#### Создание таблицы
```javascript
// Frontend
await tablesService.createTable({
name: "Контакты",
description: "База клиентов",
isRagSourceId: 2 // Источник для AI
});
// Backend: POST /tables
// Шифрует name и description с AES-256
```
#### Добавление столбца
```javascript
await tablesService.addColumn(tableId, {
name: "Email",
type: "text",
order: 2,
purpose: "contact" // Для специальных полей
});
// Backend: POST /tables/:id/columns
// Генерирует уникальный placeholder: "email", "email_1", ...
```
#### Добавление строки
```javascript
await tablesService.addRow(tableId);
// Backend: POST /tables/:id/rows
// Автоматически индексирует в vector store для AI
```
#### Обновление ячейки (Upsert)
```javascript
await tablesService.saveCell({
row_id: 123,
column_id: 5,
value: "new@email.com"
});
// Backend: POST /tables/cell
// INSERT ... ON CONFLICT ... DO UPDATE
// Автоматически обновляет vector store
```
#### Удаление строки
```javascript
await tablesService.deleteRow(rowId);
// Backend: DELETE /tables/row/:rowId
// Пересобирает vector store (rebuild)
```
#### Удаление столбца
```javascript
await tablesService.deleteColumn(columnId);
// Backend: DELETE /tables/column/:columnId
// Каскадно удаляет:
// 1. Все связи (user_table_relations)
// 2. Все значения ячеек (user_cell_values)
// 3. Сам столбец
```
#### Удаление таблицы
```javascript
await tablesService.deleteTable(tableId);
// Backend: DELETE /tables/:id
// Требуется: req.session.userAccessLevel?.hasAccess
// Каскадно удаляет все связанные данные
```
### 2. Фильтрация данных
#### По продукту
```javascript
GET /tables/5/rows?product=Premium
// Backend фильтрует строки:
filtered = rows.filter(r => r.product === 'Premium');
```
#### По тегам
```javascript
GET /tables/5/rows?tags=VIP,B2B
// Backend фильтрует строки с любым из тегов:
filtered = rows.filter(r =>
r.userTags.includes('VIP') || r.userTags.includes('B2B')
);
```
#### По связям (Relation)
```javascript
GET /tables/5/rows?relation_12=45,46
// Фильтр строк, связанных с to_row_id = 45 или 46
// через столбец column_id = 12
```
#### По мультивыбору (Multiselect)
```javascript
GET /tables/5/rows?multiselect_8=10,11,12
// Все выбранные значения должны присутствовать
```
### 3. Placeholder система
**Автоматическая генерация**:
```javascript
// Функция: generatePlaceholder(name, existingPlaceholders)
"Имя клиента" "imya_klienta"
"Email" "email"
"Email" (2-й раз) "email_1"
"123" "column" (fallback)
"Цена-$" "tsena"
```
**Транслитерация**:
```javascript
const cyrillicToLatinMap = {
а: 'a', б: 'b', в: 'v', г: 'g', д: 'd',
е: 'e', ё: 'e', ж: 'zh', з: 'z', и: 'i',
// ... полная карта
};
```
**Использование**:
```javascript
// API доступ к данным через placeholder
GET /tables/5/data?fields=email,phone,imya_klienta
```
### 4. Порядок строк (Order)
```javascript
// Изменение порядка строк (drag-n-drop)
await tablesService.updateRowsOrder(tableId, [
{ rowId: 100, order: 0 },
{ rowId: 101, order: 1 },
{ rowId: 102, order: 2 }
]);
// Backend: PATCH /tables/:id/rows/order
// Обновляет поле "order" для каждой строки
```
### 5. Real-time обновления (WebSocket)
```javascript
// Backend отправляет уведомления при изменениях
broadcastTableUpdate(tableId); // Обновление таблицы
broadcastTableRelationsUpdate(); // Обновление связей
broadcastTagsUpdate(null, rowId); // Обновление тегов
// Frontend подписывается на события
socket.on('table-update', (data) => {
if (data.tableId === currentTableId) {
reloadTableData();
}
});
```
### 6. Массовые операции
```javascript
// Выбор нескольких строк (checkbox)
const selectedRows = [100, 101, 102];
// Массовое удаление
for (const rowId of selectedRows) {
await tablesService.deleteRow(rowId);
}
// После удаления: автоматический rebuild vector store
```
---
## Связи между таблицами
### Типы связей
#### 1. One-to-Many (N:1) - Relation
**Пример**: Задачи → Проекты
```
Таблица "Задачи" Таблица "Проекты"
├── task_1 → project_id=5 ├── project_5 (Сайт)
├── task_2 → project_id=5 └── project_6 (API)
└── task_3 → project_id=6
```
**Хранение**:
```sql
user_table_relations
├── from_row_id=task_1, column_id=3, to_table_id=2, to_row_id=project_5
├── from_row_id=task_2, column_id=3, to_table_id=2, to_row_id=project_5
└── from_row_id=task_3, column_id=3, to_table_id=2, to_row_id=project_6
```
#### 2. Many-to-Many (N:N) - Multiselect-Relation
**Пример**: Контакты → Теги
```
Таблица "Контакты" Таблица "Теги"
├── contact_1 → [VIP, B2B] ├── tag_1 (VIP)
├── contact_2 → [VIP] ├── tag_2 (B2B)
└── contact_3 → [B2B, Local] └── tag_3 (Local)
```
**Хранение**:
```sql
user_table_relations
├── from_row_id=contact_1, column_id=5, to_table_id=3, to_row_id=tag_1
├── from_row_id=contact_1, column_id=5, to_table_id=3, to_row_id=tag_2
├── from_row_id=contact_2, column_id=5, to_table_id=3, to_row_id=tag_1
├── from_row_id=contact_3, column_id=5, to_table_id=3, to_row_id=tag_2
└── from_row_id=contact_3, column_id=5, to_table_id=3, to_row_id=tag_3
```
#### 3. Lookup (Подстановка)
**Пример**: Заказы → Цена товара
```
Таблица "Заказы"
├── order_id (text)
├── product (relation → Товары)
└── price (lookup → Товары.price)
Таблица "Товары"
├── product_name (text)
└── price (number)
```
**Как работает**:
1. Выбираете `product = "Ноутбук"` (связь с товаром)
2. `price` автоматически подставляется из `Товары.price`
3. Если цена товара изменится, lookup обновится
### API для работы со связями
```javascript
// Получить все связи строки
GET /tables/:tableId/row/:rowId/relations
// Добавить связь
POST /tables/:tableId/row/:rowId/relations
Body: {
column_id: 12,
to_table_id: 5,
to_row_id: 45
}
// Добавить несколько связей (multiselect)
POST /tables/:tableId/row/:rowId/relations
Body: {
column_id: 12,
to_table_id: 5,
to_row_ids: [45, 46, 47]
}
// Удалить связь
DELETE /tables/:tableId/row/:rowId/relations/:relationId
```
---
## Интеграция с AI (RAG)
### Векторный поиск
Таблицы используются как **база знаний для AI ассистента**.
#### Автоматическая индексация
**При создании/изменении строки**:
```javascript
// Backend: POST /tables/:id/rows
const rows = await getTableRows(tableId);
const upsertRows = rows
.filter(r => r.row_id && r.text)
.map(r => ({
row_id: r.row_id,
text: r.text, // Вопрос (question column)
metadata: {
answer: r.answer, // Ответ (answer column)
product: r.product, // Фильтр по продукту
userTags: r.userTags, // Фильтр по тегам
priority: r.priority // Приоритет
}
}));
if (upsertRows.length > 0) {
await vectorSearchClient.upsert(tableId, upsertRows);
}
```
**При удалении строки**:
```javascript
// Backend: DELETE /tables/row/:rowId
const rows = await getTableRows(tableId);
const rebuildRows = /* ... */;
if (rebuildRows.length > 0) {
await vectorSearchClient.rebuild(tableId, rebuildRows);
}
```
#### Специальные поля для RAG
```javascript
// Столбцы с purpose
{
"type": "text",
"options": {
"purpose": "question" // Вопрос для AI
}
}
{
"type": "text",
"options": {
"purpose": "answer" // Ответ AI
}
}
{
"type": "multiselect",
"options": {
"purpose": "product" // Фильтр по продукту
}
}
{
"type": "multiselect",
"options": {
"purpose": "userTags" // Фильтр по тегам
}
}
```
#### Ручная пересборка индекса
```javascript
// Frontend (только для админов)
await tablesService.rebuildIndex(tableId);
// Backend: POST /tables/:id/rebuild-index
// Требуется: req.session.userAccessLevel?.hasAccess
const { questionCol, answerCol } = await getQuestionAnswerColumnIds(tableId);
const rows = await getRowsWithQA(tableId, questionCol, answerCol);
if (rows.length > 0) {
await vectorSearchClient.rebuild(tableId, rows);
}
```
#### Как AI использует таблицы
```
1. Пользователь задает вопрос AI:
"Как вернуть товар?"
2. AI делает векторный поиск:
vectorSearch.search(tableId, query, top_k=3)
3. Находит похожие вопросы в таблице:
- row_id: 123
- text: "Как оформить возврат товара?"
- score: -250 (близко к порогу 300)
- metadata: { answer: "Возврат в течение 14 дней..." }
4. AI возвращает ответ из metadata.answer
5. Если не нашел (score > 300):
AI генерирует ответ через LLM (Ollama)
```
#### Фильтрация по продуктам и тегам
```javascript
// Поиск только по продукту "Premium"
const results = await vectorSearch.search(tableId, query, 3);
const filtered = results.filter(r => r.metadata.product === 'Premium');
// Поиск только по тегам "VIP" или "B2B"
const filtered = results.filter(r =>
r.metadata.userTags.includes('VIP') ||
r.metadata.userTags.includes('B2B')
);
```
---
## API Reference
### Таблицы
#### GET /tables
Получить список всех таблиц
**Ответ**:
```json
[
{
"id": 1,
"name": "Контакты",
"description": "База клиентов",
"is_rag_source_id": 2,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
]
```
#### POST /tables
Создать новую таблицу
**Запрос**:
```json
{
"name": "Контакты",
"description": "База клиентов",
"isRagSourceId": 2
}
```
**Ответ**: Объект созданной таблицы
#### GET /tables/:id
Получить структуру и данные таблицы
**Ответ**:
```json
{
"name": "Контакты",
"description": "База клиентов",
"columns": [
{
"id": 1,
"table_id": 1,
"name": "Email",
"type": "text",
"placeholder": "email",
"options": null,
"order": 0
}
],
"rows": [
{
"id": 100,
"table_id": 1,
"order": 0,
"created_at": "2025-01-15T10:00:00Z"
}
],
"cellValues": [
{
"id": 500,
"row_id": 100,
"column_id": 1,
"value": "user@example.com"
}
]
}
```
#### PATCH /tables/:id
Обновить метаданные таблицы
**Запрос**:
```json
{
"name": "Клиенты",
"description": "Обновленное описание"
}
```
#### DELETE /tables/:id
Удалить таблицу (только для админов)
**Требования**: `req.session.userAccessLevel?.hasAccess === true`
### Столбцы
#### POST /tables/:id/columns
Добавить столбец
**Запрос**:
```json
{
"name": "Email",
"type": "text",
"order": 2,
"purpose": "contact"
}
```
#### PATCH /tables/column/:columnId
Обновить столбец
**Запрос**:
```json
{
"name": "Новое название",
"type": "text",
"order": 5
}
```
#### DELETE /tables/column/:columnId
Удалить столбец (каскадное удаление всех значений)
### Строки
#### POST /tables/:id/rows
Добавить строку
**Ответ**: Объект созданной строки
#### DELETE /tables/row/:rowId
Удалить строку
#### PATCH /tables/:id/rows/order
Изменить порядок строк
**Запрос**:
```json
{
"order": [
{ "rowId": 100, "order": 0 },
{ "rowId": 101, "order": 1 }
]
}
```
### Ячейки
#### POST /tables/cell
Создать или обновить значение ячейки (upsert)
**Запрос**:
```json
{
"row_id": 100,
"column_id": 5,
"value": "new@email.com"
}
```
**Логика**:
```sql
INSERT INTO user_cell_values (row_id, column_id, value_encrypted)
VALUES ($1, $2, encrypt_text($3, $4))
ON CONFLICT (row_id, column_id)
DO UPDATE SET value_encrypted = encrypt_text($3, $4), updated_at = NOW()
```
### Фильтрация
#### GET /tables/:id/rows
Получить отфильтрованные строки
**Параметры**:
```
?product=Premium // Фильтр по продукту
&tags=VIP,B2B // Фильтр по тегам
&relation_12=45,46 // Фильтр по связи (column_id=12)
&multiselect_8=10,11 // Фильтр по мультивыбору (column_id=8)
&lookup_15=100 // Фильтр по lookup (column_id=15)
```
### RAG индекс
#### POST /tables/:id/rebuild-index
Пересобрать векторный индекс (только для админов)
**Требования**: `req.session.userAccessLevel?.hasAccess === true`
**Ответ**:
```json
{
"success": true,
"count": 150
}
```
### Связи
#### GET /tables/:tableId/row/:rowId/relations
Получить все связи строки
**Ответ**:
```json
[
{
"id": 1000,
"from_row_id": 100,
"column_id": 12,
"to_table_id": 5,
"to_row_id": 45
}
]
```
#### POST /tables/:tableId/row/:rowId/relations
Добавить связь или связи
**Одна связь**:
```json
{
"column_id": 12,
"to_table_id": 5,
"to_row_id": 45
}
```
**Несколько связей** (multiselect):
```json
{
"column_id": 12,
"to_table_id": 5,
"to_row_ids": [45, 46, 47]
}
```
**Логика**:
- Удаляет старые связи для column_id
- Добавляет новые связи
#### DELETE /tables/:tableId/row/:rowId/relations/:relationId
Удалить связь
### Плейсхолдеры
#### GET /tables/:id/placeholders
Получить плейсхолдеры для столбцов таблицы
**Ответ**:
```json
[
{
"id": 1,
"name": "Email",
"placeholder": "email"
},
{
"id": 2,
"name": "Имя клиента",
"placeholder": "imya_klienta"
}
]
```
#### GET /tables/placeholders/all
Получить все плейсхолдеры по всем таблицам
**Ответ**:
```json
[
{
"column_id": 1,
"column_name": "Email",
"placeholder": "email",
"table_id": 1,
"table_name": "Контакты"
}
]
```
---
## Примеры использования
### Пример 1: База знаний FAQ для AI
#### Создание таблицы
```javascript
const table = await tablesService.createTable({
name: "FAQ",
description: "Часто задаваемые вопросы для AI",
isRagSourceId: 2 // RAG источник
});
```
#### Добавление столбцов
```javascript
// Вопрос (для векторного поиска)
await tablesService.addColumn(table.id, {
name: "Вопрос",
type: "text",
order: 0,
purpose: "question"
});
// Ответ (для AI)
await tablesService.addColumn(table.id, {
name: "Ответ",
type: "text",
order: 1,
purpose: "answer"
});
// Продукт (для фильтрации)
await tablesService.addColumn(table.id, {
name: "Продукт",
type: "multiselect",
order: 2,
purpose: "product",
options: {
choices: ["Базовый", "Премиум", "Корпоративный"]
}
});
// Теги (для фильтрации)
await tablesService.addColumn(table.id, {
name: "Теги",
type: "multiselect",
order: 3,
purpose: "userTags",
options: {
choices: ["Оплата", "Доставка", "Возврат", "Гарантия"]
}
});
```
#### Добавление данных
```javascript
// Добавляем строку
const row = await tablesService.addRow(table.id);
// Заполняем ячейки
await tablesService.saveCell({
row_id: row.id,
column_id: 1, // Вопрос
value: "Как вернуть товар?"
});
await tablesService.saveCell({
row_id: row.id,
column_id: 2, // Ответ
value: "Возврат товара возможен в течение 14 дней с момента покупки..."
});
// Автоматически индексируется в vector store!
```
#### Поиск через AI
```javascript
// Пользователь спрашивает AI
const userQuestion = "можно ли вернуть покупку?";
// AI делает векторный поиск
const results = await vectorSearch.search(table.id, userQuestion, 3);
// Находит похожий вопрос "Как вернуть товар?" (score: -200)
// Возвращает ответ из metadata.answer
```
### Пример 2: CRM система
#### Структура
```javascript
// Таблица "Компании"
const companies = await tablesService.createTable({
name: "Компании",
description: "База компаний"
});
await tablesService.addColumn(companies.id, {
name: "Название",
type: "text",
order: 0
});
await tablesService.addColumn(companies.id, {
name: "Сайт",
type: "text",
order: 1
});
await tablesService.addColumn(companies.id, {
name: "Отрасль",
type: "multiselect",
order: 2,
options: { choices: ["IT", "Финансы", "Ритейл", "Производство"] }
});
// Таблица "Контакты"
const contacts = await tablesService.createTable({
name: "Контакты",
description: "База контактов"
});
await tablesService.addColumn(contacts.id, {
name: "Имя",
type: "text",
order: 0
});
await tablesService.addColumn(contacts.id, {
name: "Email",
type: "text",
order: 1
});
// Связь: Контакт → Компания
await tablesService.addColumn(contacts.id, {
name: "Компания",
type: "relation",
order: 2,
options: {
relatedTableId: companies.id,
relatedColumnId: 1 // Название компании
}
});
// Lookup: Сайт компании
await tablesService.addColumn(contacts.id, {
name: "Сайт компании",
type: "lookup",
order: 3,
options: {
relatedTableId: companies.id,
relatedColumnId: 2, // Связь через "Компания"
lookupColumnId: 2 // Подставить "Сайт"
}
});
```
#### Использование
```javascript
// Добавляем компанию
const company = await tablesService.addRow(companies.id);
await tablesService.saveCell({
row_id: company.id,
column_id: 1,
value: "Microsoft"
});
await tablesService.saveCell({
row_id: company.id,
column_id: 2,
value: "https://microsoft.com"
});
// Добавляем контакт
const contact = await tablesService.addRow(contacts.id);
await tablesService.saveCell({
row_id: contact.id,
column_id: 1,
value: "John Doe"
});
// Связываем контакт с компанией
await api.post(`/tables/${contacts.id}/row/${contact.id}/relations`, {
column_id: 3, // "Компания"
to_table_id: companies.id,
to_row_id: company.id
});
// Lookup автоматически подставит "https://microsoft.com"!
```
### Пример 3: Управление задачами
#### Структура
```javascript
// Таблица "Проекты"
const projects = await tablesService.createTable({
name: "Проекты",
description: "Активные проекты"
});
await tablesService.addColumn(projects.id, {
name: "Название",
type: "text",
order: 0
});
await tablesService.addColumn(projects.id, {
name: "Статус",
type: "multiselect",
order: 1,
options: { choices: ["Планирование", "В работе", "Завершен"] }
});
// Таблица "Задачи"
const tasks = await tablesService.createTable({
name: "Задачи",
description: "Задачи по проектам"
});
await tablesService.addColumn(tasks.id, {
name: "Название",
type: "text",
order: 0
});
await tablesService.addColumn(tasks.id, {
name: "Проект",
type: "relation",
order: 1,
options: {
relatedTableId: projects.id,
relatedColumnId: 1
}
});
await tablesService.addColumn(tasks.id, {
name: "Приоритет",
type: "number",
order: 2
});
await tablesService.addColumn(tasks.id, {
name: "Статус",
type: "multiselect",
order: 3,
options: { choices: ["To Do", "In Progress", "Review", "Done"] }
});
```
#### Фильтрация задач по проекту
```javascript
// Получить все задачи проекта с ID = 5
const tasks = await api.get(`/tables/${tasks.id}/rows?relation_2=5`);
// Получить задачи с приоритетом > 5
const highPriority = tasks.filter(task => {
const priority = cellValues.find(
cell => cell.row_id === task.id && cell.column_id === 3
)?.value;
return parseInt(priority) > 5;
});
```
---
## Безопасность
### Шифрование данных
**Все чувствительные данные шифруются AES-256**:
```javascript
// Шифруются:
name_encrypted // Название таблицы
description_encrypted // Описание
value_encrypted // Значения ячеек
placeholder_encrypted // Плейсхолдеры
// НЕ шифруются (для индексов и производительности):
placeholder // Незашифрованный плейсхолдер
options // JSONB настройки
order // Порядок
```
**Функции шифрования в PostgreSQL**:
```sql
-- Шифрование
encrypt_text(plain_text, encryption_key)
-- Расшифровка
decrypt_text(encrypted_text, encryption_key)
-- Пример использования
INSERT INTO user_tables (name_encrypted)
VALUES (encrypt_text('Контакты', $1));
SELECT decrypt_text(name_encrypted, $1) as name
FROM user_tables;
```
### Права доступа
```javascript
// Просмотр: все авторизованные пользователи
GET /tables
GET /tables/:id
GET /tables/:id/rows
// Редактирование: пользователи с правами
if (!canEditData) {
return res.status(403).json({ error: 'Доступ запрещен' });
}
POST /tables/:id/columns
POST /tables/:id/rows
POST /tables/cell
PATCH /tables/column/:columnId
// Удаление: только администраторы
if (!req.session.userAccessLevel?.hasAccess) {
return res.status(403).json({ error: 'Только для администраторов' });
}
DELETE /tables/:id
DELETE /tables/column/:columnId
DELETE /tables/row/:rowId
POST /tables/:id/rebuild-index
```
### Проверка прав через токены
```javascript
// Backend проверяет баланс токенов
const address = req.session.address;
const dleContract = new ethers.Contract(dleAddress, dleAbi, provider);
const balance = await dleContract.balanceOf(address);
if (balance === 0n) {
return res.status(403).json({
error: 'Доступ запрещен: нет токенов'
});
}
// Определяем уровень доступа
const accessLevel = determineAccessLevel(balance);
req.session.userAccessLevel = accessLevel;
```
### Защита от SQL-инъекций
**Параметризованные запросы**:
```javascript
// ✅ Безопасно (параметры)
await db.getQuery()(
'SELECT * FROM user_tables WHERE id = $1',
[tableId]
);
// ❌ ОПАСНО (конкатенация)
await db.getQuery()(
`SELECT * FROM user_tables WHERE id = ${tableId}`
);
```
### Валидация входных данных
```javascript
// Проверка типов
if (typeof name !== 'string') {
return res.status(400).json({ error: 'Invalid name' });
}
// Проверка существования
const exists = await db.getQuery()(
'SELECT id FROM user_tables WHERE id = $1',
[tableId]
);
if (!exists.rows[0]) {
return res.status(404).json({ error: 'Table not found' });
}
// Проверка уникальности (placeholder)
const duplicate = await db.getQuery()(
'SELECT id FROM user_columns WHERE placeholder = $1 AND id != $2',
[placeholder, columnId]
);
if (duplicate.rows.length > 0) {
placeholder = generateUniquePlaceholder();
}
```
### Каскадное удаление (защита от orphaned data)
```sql
-- Все связи с ON DELETE CASCADE
CREATE TABLE user_columns (
table_id INTEGER NOT NULL
REFERENCES user_tables(id) ON DELETE CASCADE
);
CREATE TABLE user_rows (
table_id INTEGER NOT NULL
REFERENCES user_tables(id) ON DELETE CASCADE
);
CREATE TABLE user_cell_values (
row_id INTEGER NOT NULL
REFERENCES user_rows(id) ON DELETE CASCADE,
column_id INTEGER NOT NULL
REFERENCES user_columns(id) ON DELETE CASCADE
);
-- Результат: удаление таблицы автоматически удаляет ВСЁ
```
### Rate Limiting
```javascript
// В backend/routes/tables.js можно добавить
const rateLimit = require('express-rate-limit');
const tablesLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100, // 100 запросов
message: 'Слишком много запросов к таблицам'
});
router.use(tablesLimiter);
```
---
## Производительность
### Оптимизации
#### 1. Параллельные запросы
```javascript
// Вместо последовательных запросов:
const tableMeta = await db.query('SELECT ...');
const columns = await db.query('SELECT ...');
const rows = await db.query('SELECT ...');
const cellValues = await db.query('SELECT ...');
// Используются параллельные:
const [tableMeta, columns, rows, cellValues] = await Promise.all([
db.query('SELECT ...'),
db.query('SELECT ...'),
db.query('SELECT ...'),
db.query('SELECT ...')
]);
// Ускорение: 4x
```
#### 2. Индексы на связях
```sql
CREATE INDEX idx_user_table_relations_from_row
ON user_table_relations(from_row_id);
CREATE INDEX idx_user_table_relations_to_table
ON user_table_relations(to_table_id);
-- Результат: быстрая фильтрация по связям
```
#### 3. UNIQUE constraint
```sql
CREATE TABLE user_cell_values (
...
UNIQUE(row_id, column_id)
);
-- Преимущества:
-- 1. Предотвращает дубликаты ячеек
-- 2. Ускоряет upsert (ON CONFLICT)
-- 3. Автоматический индекс
```
#### 4. WebSocket вместо polling
```javascript
// ❌ Polling (медленно)
setInterval(async () => {
const data = await fetchTableData();
updateUI(data);
}, 5000);
// ✅ WebSocket (мгновенно)
socket.on('table-update', (data) => {
if (data.tableId === currentTableId) {
updateUI(data);
}
});
// Результат: real-time обновления, нет нагрузки на сервер
```
#### 5. Кэширование
```javascript
// Backend может добавить кэш для часто запрашиваемых таблиц
const NodeCache = require('node-cache');
const tableCache = new NodeCache({ stdTTL: 300 }); // 5 минут
router.get('/:id', async (req, res) => {
const cacheKey = `table_${req.params.id}`;
const cached = tableCache.get(cacheKey);
if (cached) {
return res.json(cached);
}
const data = await fetchTableData(req.params.id);
tableCache.set(cacheKey, data);
res.json(data);
});
```
### Метрики
**Типичное время ответа**:
```
GET /tables → 50-100ms (все таблицы)
GET /tables/:id → 150-300ms (с данными, Promise.all)
POST /tables/cell → 100-200ms (upsert + vector update)
DELETE /tables/row/:id → 200-400ms (удаление + rebuild vector)
POST /tables/:id/rebuild-index → 1-5s (зависит от размера)
```
**Оптимальные размеры таблиц**:
```
Строк: до 10,000 → Отлично
Строк: 10,000-50,000 → Хорошо
Строк: >50,000 → Нужны доп. оптимизации (pagination, lazy load)
```
---
## Ограничения и будущие улучшения
### Текущие ограничения
1. **Нет пагинации**: Все строки загружаются сразу
- Для больших таблиц (>1000 строк) может быть медленно
2. **Нет формул**: Нельзя делать вычисляемые поля
- Workaround: использовать lookup
3. **Нет группировки**: Нельзя группировать строки
- Workaround: фильтрация на frontend
4. **Нет истории изменений**: Не отслеживается, кто и когда изменил
- Можно добавить audit trail
5. **Ограниченная сортировка**: Только через order поле
- Нет сортировки по столбцам на backend
### Возможные улучшения
```javascript
// 1. Пагинация
GET /tables/:id/rows?page=1&limit=50
// 2. Сортировка
GET /tables/:id/rows?sort_by=column_id&order=asc
// 3. Формулы
{
"type": "formula",
"options": {
"formula": "{{price}} * {{quantity}}"
}
}
// 4. История изменений
CREATE TABLE user_cell_history (
id SERIAL PRIMARY KEY,
cell_id INTEGER REFERENCES user_cell_values(id),
old_value TEXT,
new_value TEXT,
changed_by INTEGER,
changed_at TIMESTAMP
);
// 5. Экспорт/импорт
POST /tables/:id/export CSV/Excel
POST /tables/:id/import CSV/Excel
// 6. Шаблоны таблиц
POST /tables/templates/crm Создать CRM из шаблона
POST /tables/templates/tasks Создать Kanban из шаблона
```
---
## Заключение
Система электронных таблиц в DLE — это **мощный инструмент** для управления структурированными данными с:
**Гибкой структурой** (6 типов полей)
**Связями между таблицами** (relation, lookup)
**AI интеграцией** (RAG, векторный поиск)
**Real-time обновлениями** (WebSocket)
**Безопасностью** (AES-256, права доступа)
**Производительностью** (индексы, параллельные запросы)
Это **не просто Excel**, а **полноценная база данных** с удобным интерфейсом и AI ассистентом!
---
**© 2024-2025 Тарабанов Александр Викторович. Все права защищены.**
**Версия документа**: 1.0.0
**Дата создания**: October 25, 2025
**Статус**: Временный (для внутреннего использования)