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

45 KiB
Raw Blame History

English | Русский

Система электронных таблиц в DLE

Временный документ для внутреннего анализа


📋 Содержание

  1. Обзор системы
  2. Архитектура базы данных
  3. Типы полей
  4. Функциональные возможности
  5. Связи между таблицами
  6. Интеграция с AI (RAG)
  7. 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)

┌──────────────────────────────────────────────────────────┐
                    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 для автоматической очистки.

Индексы для производительности

-- Индексы на связях (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 (Текст)

Описание: Обычное текстовое поле

{
  "type": "text",
  "options": null
}

Использование:

  • Имена
  • Описания
  • Email
  • URL
  • Любой текст

2. Number (Число)

Описание: Числовое поле

{
  "type": "number",
  "options": null
}

Использование:

  • Цены
  • Количество
  • Рейтинги
  • Проценты

3. Multiselect (Множественный выбор)

Описание: Выбор нескольких значений из списка

{
  "type": "multiselect",
  "options": {
    "choices": ["Вариант 1", "Вариант 2", "Вариант 3"]
  }
}

Использование:

  • Теги
  • Категории
  • Статусы
  • Навыки

4. Multiselect-Relation (Мультивыбор из таблицы)

Описание: Множественный выбор строк из другой таблицы

{
  "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)

{
  "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 (Подстановка)

Описание: Автоматическая подстановка значения из связанной таблицы

{
  "type": "lookup",
  "options": {
    "relatedTableId": 4,
    "relatedColumnId": 10,
    "lookupColumnId": 11  // Какое поле подставлять
  }
}

Пример:

Таблица "Заказы"
├── order_id (text)
├── product (relation → Продукты)
└── product_price (lookup → Продукты.price)

Когда выбираешь product, автоматически подставляется цена!

Использование:

  • Цены из справочника
  • Email из контактов
  • Статусы из связанных задач

Функциональные возможности

1. CRUD операции

Создание таблицы

// Frontend
await tablesService.createTable({
  name: "Контакты",
  description: "База клиентов",
  isRagSourceId: 2  // Источник для AI
});

// Backend: POST /tables
// Шифрует name и description с AES-256

Добавление столбца

await tablesService.addColumn(tableId, {
  name: "Email",
  type: "text",
  order: 2,
  purpose: "contact"  // Для специальных полей
});

// Backend: POST /tables/:id/columns
// Генерирует уникальный placeholder: "email", "email_1", ...

Добавление строки

await tablesService.addRow(tableId);

// Backend: POST /tables/:id/rows
// Автоматически индексирует в vector store для AI

Обновление ячейки (Upsert)

await tablesService.saveCell({
  row_id: 123,
  column_id: 5,
  value: "new@email.com"
});

// Backend: POST /tables/cell
// INSERT ... ON CONFLICT ... DO UPDATE
// Автоматически обновляет vector store

Удаление строки

await tablesService.deleteRow(rowId);

// Backend: DELETE /tables/row/:rowId
// Пересобирает vector store (rebuild)

Удаление столбца

await tablesService.deleteColumn(columnId);

// Backend: DELETE /tables/column/:columnId
// Каскадно удаляет:
// 1. Все связи (user_table_relations)
// 2. Все значения ячеек (user_cell_values)
// 3. Сам столбец

Удаление таблицы

await tablesService.deleteTable(tableId);

// Backend: DELETE /tables/:id
// Требуется: req.session.userAccessLevel?.hasAccess
// Каскадно удаляет все связанные данные

2. Фильтрация данных

По продукту

GET /tables/5/rows?product=Premium

// Backend фильтрует строки:
filtered = rows.filter(r => r.product === 'Premium');

По тегам

GET /tables/5/rows?tags=VIP,B2B

// Backend фильтрует строки с любым из тегов:
filtered = rows.filter(r => 
  r.userTags.includes('VIP') || r.userTags.includes('B2B')
);

По связям (Relation)

GET /tables/5/rows?relation_12=45,46

// Фильтр строк, связанных с to_row_id = 45 или 46
// через столбец column_id = 12

По мультивыбору (Multiselect)

GET /tables/5/rows?multiselect_8=10,11,12

// Все выбранные значения должны присутствовать

3. Placeholder система

Автоматическая генерация:

// Функция: generatePlaceholder(name, existingPlaceholders)

"Имя клиента"      "imya_klienta"
"Email"            "email"
"Email" (2-й раз)  "email_1"
"123"              "column"  (fallback)
"Цена-$"           "tsena"

Транслитерация:

const cyrillicToLatinMap = {
  а: 'a', б: 'b', в: 'v', г: 'g', д: 'd',
  е: 'e', ё: 'e', ж: 'zh', з: 'z', и: 'i',
  // ... полная карта
};

Использование:

// API доступ к данным через placeholder
GET /tables/5/data?fields=email,phone,imya_klienta

4. Порядок строк (Order)

// Изменение порядка строк (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)

// Backend отправляет уведомления при изменениях
broadcastTableUpdate(tableId);           // Обновление таблицы
broadcastTableRelationsUpdate();         // Обновление связей
broadcastTagsUpdate(null, rowId);        // Обновление тегов

// Frontend подписывается на события
socket.on('table-update', (data) => {
  if (data.tableId === currentTableId) {
    reloadTableData();
  }
});

6. Массовые операции

// Выбор нескольких строк (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

Хранение:

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)

Хранение:

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 для работы со связями

// Получить все связи строки
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 ассистента.

Автоматическая индексация

При создании/изменении строки:

// 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);
}

При удалении строки:

// Backend: DELETE /tables/row/:rowId
const rows = await getTableRows(tableId);
const rebuildRows = /* ... */;

if (rebuildRows.length > 0) {
  await vectorSearchClient.rebuild(tableId, rebuildRows);
}

Специальные поля для RAG

// Столбцы с purpose
{
  "type": "text",
  "options": {
    "purpose": "question"  // Вопрос для AI
  }
}

{
  "type": "text",
  "options": {
    "purpose": "answer"  // Ответ AI
  }
}

{
  "type": "multiselect",
  "options": {
    "purpose": "product"  // Фильтр по продукту
  }
}

{
  "type": "multiselect",
  "options": {
    "purpose": "userTags"  // Фильтр по тегам
  }
}

Ручная пересборка индекса

// 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)

Фильтрация по продуктам и тегам

// Поиск только по продукту "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

Получить список всех таблиц

Ответ:

[
  {
    "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

Создать новую таблицу

Запрос:

{
  "name": "Контакты",
  "description": "База клиентов",
  "isRagSourceId": 2
}

Ответ: Объект созданной таблицы

GET /tables/:id

Получить структуру и данные таблицы

Ответ:

{
  "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

Обновить метаданные таблицы

Запрос:

{
  "name": "Клиенты",
  "description": "Обновленное описание"
}

DELETE /tables/:id

Удалить таблицу (только для админов)

Требования: req.session.userAccessLevel?.hasAccess === true

Столбцы

POST /tables/:id/columns

Добавить столбец

Запрос:

{
  "name": "Email",
  "type": "text",
  "order": 2,
  "purpose": "contact"
}

PATCH /tables/column/:columnId

Обновить столбец

Запрос:

{
  "name": "Новое название",
  "type": "text",
  "order": 5
}

DELETE /tables/column/:columnId

Удалить столбец (каскадное удаление всех значений)

Строки

POST /tables/:id/rows

Добавить строку

Ответ: Объект созданной строки

DELETE /tables/row/:rowId

Удалить строку

PATCH /tables/:id/rows/order

Изменить порядок строк

Запрос:

{
  "order": [
    { "rowId": 100, "order": 0 },
    { "rowId": 101, "order": 1 }
  ]
}

Ячейки

POST /tables/cell

Создать или обновить значение ячейки (upsert)

Запрос:

{
  "row_id": 100,
  "column_id": 5,
  "value": "new@email.com"
}

Логика:

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

Ответ:

{
  "success": true,
  "count": 150
}

Связи

GET /tables/:tableId/row/:rowId/relations

Получить все связи строки

Ответ:

[
  {
    "id": 1000,
    "from_row_id": 100,
    "column_id": 12,
    "to_table_id": 5,
    "to_row_id": 45
  }
]

POST /tables/:tableId/row/:rowId/relations

Добавить связь или связи

Одна связь:

{
  "column_id": 12,
  "to_table_id": 5,
  "to_row_id": 45
}

Несколько связей (multiselect):

{
  "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

Получить плейсхолдеры для столбцов таблицы

Ответ:

[
  {
    "id": 1,
    "name": "Email",
    "placeholder": "email"
  },
  {
    "id": 2,
    "name": "Имя клиента",
    "placeholder": "imya_klienta"
  }
]

GET /tables/placeholders/all

Получить все плейсхолдеры по всем таблицам

Ответ:

[
  {
    "column_id": 1,
    "column_name": "Email",
    "placeholder": "email",
    "table_id": 1,
    "table_name": "Контакты"
  }
]

Примеры использования

Пример 1: База знаний FAQ для AI

Создание таблицы

const table = await tablesService.createTable({
  name: "FAQ",
  description: "Часто задаваемые вопросы для AI",
  isRagSourceId: 2  // RAG источник
});

Добавление столбцов

// Вопрос (для векторного поиска)
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: ["Оплата", "Доставка", "Возврат", "Гарантия"]
  }
});

Добавление данных

// Добавляем строку
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

// Пользователь спрашивает AI
const userQuestion = "можно ли вернуть покупку?";

// AI делает векторный поиск
const results = await vectorSearch.search(table.id, userQuestion, 3);

// Находит похожий вопрос "Как вернуть товар?" (score: -200)
// Возвращает ответ из metadata.answer

Пример 2: CRM система

Структура

// Таблица "Компании"
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    // Подставить "Сайт"
  }
});

Использование

// Добавляем компанию
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: Управление задачами

Структура

// Таблица "Проекты"
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"] }
});

Фильтрация задач по проекту

// Получить все задачи проекта с 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:

// Шифруются:
name_encrypted          // Название таблицы
description_encrypted   // Описание
value_encrypted         // Значения ячеек
placeholder_encrypted   // Плейсхолдеры

// НЕ шифруются (для индексов и производительности):
placeholder             // Незашифрованный плейсхолдер
options                 // JSONB настройки
order                   // Порядок

Функции шифрования в PostgreSQL:

-- Шифрование
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;

Права доступа

// Просмотр: все авторизованные пользователи
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

Проверка прав через токены

// 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-инъекций

Параметризованные запросы:

// ✅ Безопасно (параметры)
await db.getQuery()(
  'SELECT * FROM user_tables WHERE id = $1',
  [tableId]
);

// ❌ ОПАСНО (конкатенация)
await db.getQuery()(
  `SELECT * FROM user_tables WHERE id = ${tableId}`
);

Валидация входных данных

// Проверка типов
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)

-- Все связи с 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

// В 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. Параллельные запросы

// Вместо последовательных запросов:
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. Индексы на связях

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

CREATE TABLE user_cell_values (
  ...
  UNIQUE(row_id, column_id)
);

-- Преимущества:
-- 1. Предотвращает дубликаты ячеек
-- 2. Ускоряет upsert (ON CONFLICT)
-- 3. Автоматический индекс

4. WebSocket вместо polling

// ❌ 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. Кэширование

// 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

Возможные улучшения

// 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
Статус: Временный (для внутреннего использования)