[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-2026 Тарабанов Александр Викторович. Все права защищены.** **Версия документа**: 1.0.0 **Дата создания**: February 28, 2026 **Статус**: Временный (для внутреннего использования)