45 KiB
Система электронных таблиц в DLE
Временный документ для внутреннего анализа
📋 Содержание
- Обзор системы
- Архитектура базы данных
- Типы полей
- Функциональные возможности
- Связи между таблицами
- Интеграция с AI (RAG)
- API Reference
- Примеры использования
- Безопасность
Обзор системы
Что это?
Система электронных таблиц в 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
}
Использование:
- Имена
- Описания
- 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)
Как работает:
- Выбираете
product = "Ноутбук"(связь с товаром) priceавтоматически подставляется изТовары.price- Если цена товара изменится, 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)
Ограничения и будущие улучшения
Текущие ограничения
-
Нет пагинации: Все строки загружаются сразу
- Для больших таблиц (>1000 строк) может быть медленно
-
Нет формул: Нельзя делать вычисляемые поля
- Workaround: использовать lookup
-
Нет группировки: Нельзя группировать строки
- Workaround: фильтрация на frontend
-
Нет истории изменений: Не отслеживается, кто и когда изменил
- Можно добавить audit trail
-
Ограниченная сортировка: Только через 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-2025 Тарабанов Александр Викторович. Все права защищены.
Версия документа: 1.0.0
Дата создания: October 25, 2025
Статус: Временный (для внутреннего использования)