[English](https://hb3-accelerator.com/gitea/VC-HB3-Accelerator/Docs/src/branch/main/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-2026 Тарабанов Александр Викторович. Все права защищены.**

**Версия документа**: 1.0.0  
**Дата создания**: February 28, 2026  
**Статус**: Временный (для внутреннего использования)

