36 KiB
Electronic Tables System in DLE
Temporary document for internal analysis
📋 Table of Contents
- System Overview
- Database Architecture
- Field Types
- Functional Capabilities
- Relations Between Tables
- AI Integration (RAG)
- API Reference
- Usage Examples
- Security
System Overview
What is it?
The electronic tables system in DLE is a full-featured database with graphical interface, similar to Notion Database or Airtable, built into the application.
Key Features
┌─────────────────────────────────────────────────────────┐
│ DLE Electronic Tables │
├─────────────────────────────────────────────────────────┤
│ │
│ ✅ 6 field types (text, number, relation, lookup, etc.)│
│ ✅ Relations between tables (1:1, 1:N, N:N) │
│ ✅ Lookup and data substitution │
│ ✅ Filtering and sorting │
│ ✅ Real-time updates (WebSocket) │
│ ✅ AI integration (RAG for search) │
│ ✅ Encryption of all data (AES-256) │
│ ✅ Placeholders for API access │
│ ✅ Cascading deletion │
│ ✅ Bulk operations │
│ │
└─────────────────────────────────────────────────────────┘
Differences from Excel/Google Sheets
| Feature | Excel/Sheets | DLE Tables |
|---|---|---|
| Data typing | Weak | Strict (6 types) |
| Relations between tables | No | Yes (relation, lookup) |
| AI search | No | Yes (RAG, vector search) |
| Real-time updates | Partial | Full (WebSocket) |
| Encryption | No | AES-256 for all data |
| API access | Limited | Full REST API |
| Access rights | Basic | Detailed (by roles) |
Database Architecture
Table Schema (PostgreSQL)
┌──────────────────────────────────────────────────────────┐
│ user_tables │
├──────────────────────────────────────────────────────────┤
│ id SERIAL PRIMARY KEY │
│ name_encrypted TEXT NOT NULL │
│ description_encrypted TEXT │
│ is_rag_source_id INTEGER (link to 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 (for API) │
│ placeholder TEXT (unencrypted) │
│ options JSONB (settings) │
│ order INTEGER (display order) │
│ created_at TIMESTAMP │
│ updated_at TIMESTAMP │
└──────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ user_rows │
├──────────────────────────────────────────────────────────┤
│ id SERIAL PRIMARY KEY │
│ table_id INTEGER → user_tables(id) │
│ order INTEGER (row order) │
│ 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 (encrypted value) │
│ created_at TIMESTAMP │
│ updated_at TIMESTAMP │
│ UNIQUE(row_id, column_id) ← One cell = one value │
└──────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ 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 │
└──────────────────────────────────────────────────────────┘
Cascading Deletion
Table deletion (user_tables)
↓
├── Deletes all columns (user_columns)
├── Deletes all rows (user_rows)
│ └── Deletes all cell values (user_cell_values)
└── Deletes all relations (user_table_relations)
Important: Uses ON DELETE CASCADE for automatic cleanup.
Indexes for Performance
-- Indexes on relations (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);
Effect: Fast filtering and search by related tables.
Field Types
1. Text
Description: Regular text field
{
"type": "text",
"options": null
}
Usage:
- Names
- Descriptions
- URL
- Any text
2. Number
Description: Numeric field
{
"type": "number",
"options": null
}
Usage:
- Prices
- Quantities
- Ratings
- Percentages
3. Multiselect
Description: Multiple value selection from list
{
"type": "multiselect",
"options": {
"choices": ["Option 1", "Option 2", "Option 3"]
}
}
Usage:
- Tags
- Categories
- Statuses
- Skills
4. Multiselect-Relation
Description: Multiple row selection from another table
{
"type": "multiselect-relation",
"options": {
"relatedTableId": 5,
"relatedColumnId": 12
}
}
Usage:
- Relation Contacts → Tags (N:N)
- Relation Tasks → Assignees (N:N)
- Relation Products → Categories (N:N)
Storage: In user_table_relations table, multiple records created:
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
Description: Relation with one row from another table (1:1 or 1:N)
{
"type": "relation",
"options": {
"relatedTableId": 3,
"relatedColumnId": 8
}
}
Usage:
- Task → Project (N:1)
- Contact → Company (N:1)
- Order → Client (N:1)
Storage: In user_table_relations, one record created:
from_row_id=50, column_id=2, to_table_id=3, to_row_id=15
6. Lookup
Description: Automatic value substitution from related table
{
"type": "lookup",
"options": {
"relatedTableId": 4,
"relatedColumnId": 10,
"lookupColumnId": 11 // Which field to substitute
}
}
Example:
Table "Orders"
├── order_id (text)
├── product (relation → Products)
└── product_price (lookup → Products.price)
When you select product, price is automatically substituted!
Usage:
- Prices from catalog
- Email from contacts
- Statuses from related tasks
Functional Capabilities
1. CRUD Operations
Create Table
// Frontend
await tablesService.createTable({
name: "Contacts",
description: "Customer database",
isRagSourceId: 2 // Source for AI
});
// Backend: POST /tables
// Encrypts name and description with AES-256
Add Column
await tablesService.addColumn(tableId, {
name: "Email",
type: "text",
order: 2,
purpose: "contact" // For special fields
});
// Backend: POST /tables/:id/columns
// Generates unique placeholder: "email", "email_1", ...
Add Row
await tablesService.addRow(tableId);
// Backend: POST /tables/:id/rows
// Automatically indexes in vector store for AI
Update Cell (Upsert)
await tablesService.saveCell({
row_id: 123,
column_id: 5,
value: "new@email.com"
});
// Backend: POST /tables/cell
// INSERT ... ON CONFLICT ... DO UPDATE
// Automatically updates vector store
Delete Row
await tablesService.deleteRow(rowId);
// Backend: DELETE /tables/row/:rowId
// Rebuilds vector store (rebuild)
Delete Column
await tablesService.deleteColumn(columnId);
// Backend: DELETE /tables/column/:columnId
// Cascading deletion:
// 1. All relations (user_table_relations)
// 2. All cell values (user_cell_values)
// 3. Column itself
Delete Table
await tablesService.deleteTable(tableId);
// Backend: DELETE /tables/:id
// Required: req.session.userAccessLevel?.hasAccess
// Cascading deletion of all related data
2. Data Filtering
By Product
GET /tables/5/rows?product=Premium
// Backend filters rows:
filtered = rows.filter(r => r.product === 'Premium');
By Tags
GET /tables/5/rows?tags=VIP,B2B
// Backend filters rows with any of tags:
filtered = rows.filter(r =>
r.userTags.includes('VIP') || r.userTags.includes('B2B')
);
By Relations
GET /tables/5/rows?relation_12=45,46
// Filter rows related to to_row_id = 45 or 46
// through column column_id = 12
By Multiselect
GET /tables/5/rows?multiselect_8=10,11,12
// All selected values must be present
3. Placeholder System
Automatic generation:
// Function: generatePlaceholder(name, existingPlaceholders)
"Customer Name" → "customer_name"
"Email" → "email"
"Email" (2nd time) → "email_1"
"123" → "column" (fallback)
"Price-$" → "price"
Transliteration:
const cyrillicToLatinMap = {
а: 'a', б: 'b', в: 'v', г: 'g', д: 'd',
е: 'e', ё: 'e', ж: 'zh', з: 'z', и: 'i',
// ... full map
};
Usage:
// API access to data via placeholder
GET /tables/5/data?fields=email,phone,customer_name
4. Row Order
// Change row 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
// Updates "order" field for each row
5. Real-Time Updates (WebSocket)
// Backend sends notifications on changes
broadcastTableUpdate(tableId); // Table update
broadcastTableRelationsUpdate(); // Relations update
broadcastTagsUpdate(null, rowId); // Tags update
// Frontend subscribes to events
socket.on('table-update', (data) => {
if (data.tableId === currentTableId) {
reloadTableData();
}
});
6. Bulk Operations
// Select multiple rows (checkbox)
const selectedRows = [100, 101, 102];
// Bulk deletion
for (const rowId of selectedRows) {
await tablesService.deleteRow(rowId);
}
// After deletion: automatic rebuild vector store
Relations Between Tables
Relation Types
1. One-to-Many (N:1) - Relation
Example: Tasks → Projects
Table "Tasks" Table "Projects"
├── task_1 → project_id=5 ├── project_5 (Website)
├── task_2 → project_id=5 └── project_6 (API)
└── task_3 → project_id=6
Storage:
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
Example: Contacts → Tags
Table "Contacts" Table "Tags"
├── contact_1 → [VIP, B2B] ├── tag_1 (VIP)
├── contact_2 → [VIP] ├── tag_2 (B2B)
└── contact_3 → [B2B, Local] └── tag_3 (Local)
Storage:
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 (Substitution)
Example: Orders → Product Price
Table "Orders"
├── order_id (text)
├── product (relation → Products)
└── price (lookup → Products.price)
Table "Products"
├── product_name (text)
└── price (number)
How it works:
- Select
product = "Laptop"(relation to product) priceautomatically substituted fromProducts.price- If product price changes, lookup updates
API for Working with Relations
// Get all row relations
GET /tables/:tableId/row/:rowId/relations
// Add relation
POST /tables/:tableId/row/:rowId/relations
Body: {
column_id: 12,
to_table_id: 5,
to_row_id: 45
}
// Add multiple relations (multiselect)
POST /tables/:tableId/row/:rowId/relations
Body: {
column_id: 12,
to_table_id: 5,
to_row_ids: [45, 46, 47]
}
// Delete relation
DELETE /tables/:tableId/row/:rowId/relations/:relationId
AI Integration (RAG)
Vector Search
Tables are used as knowledge base for AI assistant.
Automatic Indexing
On row creation/modification:
// 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 (question column)
metadata: {
answer: r.answer, // Answer (answer column)
product: r.product, // Product filter
userTags: r.userTags, // Tags filter
priority: r.priority // Priority
}
}));
if (upsertRows.length > 0) {
await vectorSearchClient.upsert(tableId, upsertRows);
}
On row deletion:
// Backend: DELETE /tables/row/:rowId
const rows = await getTableRows(tableId);
const rebuildRows = /* ... */;
if (rebuildRows.length > 0) {
await vectorSearchClient.rebuild(tableId, rebuildRows);
}
Special Fields for RAG
// Columns with purpose
{
"type": "text",
"options": {
"purpose": "question" // Question for AI
}
}
{
"type": "text",
"options": {
"purpose": "answer" // AI Answer
}
}
{
"type": "multiselect",
"options": {
"purpose": "product" // Product filter
}
}
{
"type": "multiselect",
"options": {
"purpose": "userTags" // Tags filter
}
}
Manual Index Rebuild
// Frontend (admins only)
await tablesService.rebuildIndex(tableId);
// Backend: POST /tables/:id/rebuild-index
// Required: 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);
}
How AI Uses Tables
1. User asks AI question:
"How to return product?"
2. AI does vector search:
vectorSearch.search(tableId, query, top_k=3)
3. Finds similar questions in table:
- row_id: 123
- text: "How to process product return?"
- score: -250 (close to threshold 300)
- metadata: { answer: "Return within 14 days..." }
4. AI returns answer from metadata.answer
5. If not found (score > 300):
AI generates answer via LLM (Ollama)
Filtering by Products and Tags
// Search only by product "Premium"
const results = await vectorSearch.search(tableId, query, 3);
const filtered = results.filter(r => r.metadata.product === 'Premium');
// Search only by tags "VIP" or "B2B"
const filtered = results.filter(r =>
r.metadata.userTags.includes('VIP') ||
r.metadata.userTags.includes('B2B')
);
API Reference
Tables
GET /tables
Get list of all tables
Response:
[
{
"id": 1,
"name": "Contacts",
"description": "Customer database",
"is_rag_source_id": 2,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
]
POST /tables
Create new table
Request:
{
"name": "Contacts",
"description": "Customer database",
"isRagSourceId": 2
}
Response: Created table object
GET /tables/:id
Get table structure and data
Response:
{
"name": "Contacts",
"description": "Customer database",
"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
Update table metadata
Request:
{
"name": "Customers",
"description": "Updated description"
}
DELETE /tables/:id
Delete table (admins only)
Requirements: req.session.userAccessLevel?.hasAccess === true
Columns
POST /tables/:id/columns
Add column
Request:
{
"name": "Email",
"type": "text",
"order": 2,
"purpose": "contact"
}
PATCH /tables/column/:columnId
Update column
Request:
{
"name": "New name",
"type": "text",
"order": 5
}
DELETE /tables/column/:columnId
Delete column (cascading deletion of all values)
Rows
POST /tables/:id/rows
Add row
Response: Created row object
DELETE /tables/row/:rowId
Delete row
PATCH /tables/:id/rows/order
Change row order
Request:
{
"order": [
{ "rowId": 100, "order": 0 },
{ "rowId": 101, "order": 1 }
]
}
Cells
POST /tables/cell
Create or update cell value (upsert)
Request:
{
"row_id": 100,
"column_id": 5,
"value": "new@email.com"
}
Logic:
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()
Filtering
GET /tables/:id/rows
Get filtered rows
Parameters:
?product=Premium // Filter by product
&tags=VIP,B2B // Filter by tags
&relation_12=45,46 // Filter by relation (column_id=12)
&multiselect_8=10,11 // Filter by multiselect (column_id=8)
&lookup_15=100 // Filter by lookup (column_id=15)
RAG Index
POST /tables/:id/rebuild-index
Rebuild vector index (admins only)
Requirements: req.session.userAccessLevel?.hasAccess === true
Response:
{
"success": true,
"count": 150
}
Relations
GET /tables/:tableId/row/:rowId/relations
Get all row relations
Response:
[
{
"id": 1000,
"from_row_id": 100,
"column_id": 12,
"to_table_id": 5,
"to_row_id": 45
}
]
POST /tables/:tableId/row/:rowId/relations
Add relation or relations
Single relation:
{
"column_id": 12,
"to_table_id": 5,
"to_row_id": 45
}
Multiple relations (multiselect):
{
"column_id": 12,
"to_table_id": 5,
"to_row_ids": [45, 46, 47]
}
Logic:
- Deletes old relations for column_id
- Adds new relations
DELETE /tables/:tableId/row/:rowId/relations/:relationId
Delete relation
Placeholders
GET /tables/:id/placeholders
Get placeholders for table columns
Response:
[
{
"id": 1,
"name": "Email",
"placeholder": "email"
},
{
"id": 2,
"name": "Customer Name",
"placeholder": "customer_name"
}
]
GET /tables/placeholders/all
Get all placeholders across all tables
Response:
[
{
"column_id": 1,
"column_name": "Email",
"placeholder": "email",
"table_id": 1,
"table_name": "Contacts"
}
]
Usage Examples
Example 1: FAQ Knowledge Base for AI
Create Table
const table = await tablesService.createTable({
name: "FAQ",
description: "Frequently asked questions for AI",
isRagSourceId: 2 // RAG source
});
Add Columns
// Question (for vector search)
await tablesService.addColumn(table.id, {
name: "Question",
type: "text",
order: 0,
purpose: "question"
});
// Answer (for AI)
await tablesService.addColumn(table.id, {
name: "Answer",
type: "text",
order: 1,
purpose: "answer"
});
// Product (for filtering)
await tablesService.addColumn(table.id, {
name: "Product",
type: "multiselect",
order: 2,
purpose: "product",
options: {
choices: ["Basic", "Premium", "Enterprise"]
}
});
// Tags (for filtering)
await tablesService.addColumn(table.id, {
name: "Tags",
type: "multiselect",
order: 3,
purpose: "userTags",
options: {
choices: ["Payment", "Delivery", "Return", "Warranty"]
}
});
Add Data
// Add row
const row = await tablesService.addRow(table.id);
// Fill cells
await tablesService.saveCell({
row_id: row.id,
column_id: 1, // Question
value: "How to return product?"
});
await tablesService.saveCell({
row_id: row.id,
column_id: 2, // Answer
value: "Product return is possible within 14 days of purchase..."
});
// Automatically indexed in vector store!
Search via AI
// User asks AI
const userQuestion = "can I return my purchase?";
// AI does vector search
const results = await vectorSearch.search(table.id, userQuestion, 3);
// Finds similar question "How to return product?" (score: -200)
// Returns answer from metadata.answer
Example 2: CRM System
Structure
// Table "Companies"
const companies = await tablesService.createTable({
name: "Companies",
description: "Company database"
});
await tablesService.addColumn(companies.id, {
name: "Name",
type: "text",
order: 0
});
await tablesService.addColumn(companies.id, {
name: "Website",
type: "text",
order: 1
});
await tablesService.addColumn(companies.id, {
name: "Industry",
type: "multiselect",
order: 2,
options: { choices: ["IT", "Finance", "Retail", "Manufacturing"] }
});
// Table "Contacts"
const contacts = await tablesService.createTable({
name: "Contacts",
description: "Contact database"
});
await tablesService.addColumn(contacts.id, {
name: "Name",
type: "text",
order: 0
});
await tablesService.addColumn(contacts.id, {
name: "Email",
type: "text",
order: 1
});
// Relation: Contact → Company
await tablesService.addColumn(contacts.id, {
name: "Company",
type: "relation",
order: 2,
options: {
relatedTableId: companies.id,
relatedColumnId: 1 // Company name
}
});
// Lookup: Company Website
await tablesService.addColumn(contacts.id, {
name: "Company Website",
type: "lookup",
order: 3,
options: {
relatedTableId: companies.id,
relatedColumnId: 2, // Relation through "Company"
lookupColumnId: 2 // Substitute "Website"
}
});
Usage
// Add company
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"
});
// Add contact
const contact = await tablesService.addRow(contacts.id);
await tablesService.saveCell({
row_id: contact.id,
column_id: 1,
value: "John Doe"
});
// Link contact to company
await api.post(`/tables/${contacts.id}/row/${contact.id}/relations`, {
column_id: 3, // "Company"
to_table_id: companies.id,
to_row_id: company.id
});
// Lookup automatically substitutes "https://microsoft.com"!
Example 3: Task Management
Structure
// Table "Projects"
const projects = await tablesService.createTable({
name: "Projects",
description: "Active projects"
});
await tablesService.addColumn(projects.id, {
name: "Name",
type: "text",
order: 0
});
await tablesService.addColumn(projects.id, {
name: "Status",
type: "multiselect",
order: 1,
options: { choices: ["Planning", "In Progress", "Completed"] }
});
// Table "Tasks"
const tasks = await tablesService.createTable({
name: "Tasks",
description: "Project tasks"
});
await tablesService.addColumn(tasks.id, {
name: "Name",
type: "text",
order: 0
});
await tablesService.addColumn(tasks.id, {
name: "Project",
type: "relation",
order: 1,
options: {
relatedTableId: projects.id,
relatedColumnId: 1
}
});
await tablesService.addColumn(tasks.id, {
name: "Priority",
type: "number",
order: 2
});
await tablesService.addColumn(tasks.id, {
name: "Status",
type: "multiselect",
order: 3,
options: { choices: ["To Do", "In Progress", "Review", "Done"] }
});
Filter Tasks by Project
// Get all tasks for project with ID = 5
const tasks = await api.get(`/tables/${tasks.id}/rows?relation_2=5`);
// Get tasks with priority > 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;
});
Security
Data Encryption
All sensitive data encrypted with AES-256:
// Encrypted:
name_encrypted // Table name
description_encrypted // Description
value_encrypted // Cell values
placeholder_encrypted // Placeholders
// NOT encrypted (for indexes and performance):
placeholder // Unencrypted placeholder
options // JSONB settings
order // Order
Encryption functions in PostgreSQL:
-- Encryption
encrypt_text(plain_text, encryption_key)
-- Decryption
decrypt_text(encrypted_text, encryption_key)
-- Usage example
INSERT INTO user_tables (name_encrypted)
VALUES (encrypt_text('Contacts', $1));
SELECT decrypt_text(name_encrypted, $1) as name
FROM user_tables;
Access Rights
// View: all authorized users
GET /tables
GET /tables/:id
GET /tables/:id/rows
// Editing: users with rights
if (!canEditData) {
return res.status(403).json({ error: 'Access denied' });
}
POST /tables/:id/columns
POST /tables/:id/rows
POST /tables/cell
PATCH /tables/column/:columnId
// Deletion: administrators only
if (!req.session.userAccessLevel?.hasAccess) {
return res.status(403).json({ error: 'Administrators only' });
}
DELETE /tables/:id
DELETE /tables/column/:columnId
DELETE /tables/row/:rowId
POST /tables/:id/rebuild-index
Token-Based Rights Verification
// Backend checks token balance
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: 'Access denied: no tokens'
});
}
// Determine access level
const accessLevel = determineAccessLevel(balance);
req.session.userAccessLevel = accessLevel;
SQL Injection Protection
Parameterized queries:
// ✅ Safe (parameters)
await db.getQuery()(
'SELECT * FROM user_tables WHERE id = $1',
[tableId]
);
// ❌ DANGEROUS (concatenation)
await db.getQuery()(
`SELECT * FROM user_tables WHERE id = ${tableId}`
);
Input Validation
// Type check
if (typeof name !== 'string') {
return res.status(400).json({ error: 'Invalid name' });
}
// Existence check
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' });
}
// Uniqueness check (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();
}
Cascading Deletion (Protection from Orphaned Data)
-- All relations with 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
);
-- Result: table deletion automatically deletes EVERYTHING
Rate Limiting
// Can be added in backend/routes/tables.js
const rateLimit = require('express-rate-limit');
const tablesLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests
message: 'Too many requests to tables'
});
router.use(tablesLimiter);
Performance
Optimizations
1. Parallel Queries
// Instead of sequential queries:
const tableMeta = await db.query('SELECT ...');
const columns = await db.query('SELECT ...');
const rows = await db.query('SELECT ...');
const cellValues = await db.query('SELECT ...');
// Use parallel:
const [tableMeta, columns, rows, cellValues] = await Promise.all([
db.query('SELECT ...'),
db.query('SELECT ...'),
db.query('SELECT ...'),
db.query('SELECT ...')
]);
// Speedup: 4x
2. Indexes on Relations
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);
-- Result: fast filtering by relations
3. UNIQUE Constraint
CREATE TABLE user_cell_values (
...
UNIQUE(row_id, column_id)
);
-- Advantages:
-- 1. Prevents duplicate cells
-- 2. Speeds up upsert (ON CONFLICT)
-- 3. Automatic index
4. WebSocket Instead of Polling
// ❌ Polling (slow)
setInterval(async () => {
const data = await fetchTableData();
updateUI(data);
}, 5000);
// ✅ WebSocket (instant)
socket.on('table-update', (data) => {
if (data.tableId === currentTableId) {
updateUI(data);
}
});
// Result: real-time updates, no server load
5. Caching
// Backend can add cache for frequently requested tables
const NodeCache = require('node-cache');
const tableCache = new NodeCache({ stdTTL: 300 }); // 5 minutes
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);
});
Metrics
Typical response times:
GET /tables → 50-100ms (all tables)
GET /tables/:id → 150-300ms (with data, Promise.all)
POST /tables/cell → 100-200ms (upsert + vector update)
DELETE /tables/row/:id → 200-400ms (deletion + rebuild vector)
POST /tables/:id/rebuild-index → 1-5s (depends on size)
Optimal table sizes:
Rows: up to 10,000 → Excellent
Rows: 10,000-50,000 → Good
Rows: >50,000 → Need additional optimizations (pagination, lazy load)
Limitations and Future Improvements
Current Limitations
-
No pagination: All rows loaded at once
- For large tables (>1000 rows) may be slow
-
No formulas: Cannot create calculated fields
- Workaround: use lookup
-
No grouping: Cannot group rows
- Workaround: filtering on frontend
-
No change history: Not tracked who and when changed
- Can add audit trail
-
Limited sorting: Only through order field
- No column sorting on backend
Possible Improvements
// 1. Pagination
GET /tables/:id/rows?page=1&limit=50
// 2. Sorting
GET /tables/:id/rows?sort_by=column_id&order=asc
// 3. Formulas
{
"type": "formula",
"options": {
"formula": "{{price}} * {{quantity}}"
}
}
// 4. Change history
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. Export/import
POST /tables/:id/export → CSV/Excel
POST /tables/:id/import ← CSV/Excel
// 6. Table templates
POST /tables/templates/crm → Create CRM from template
POST /tables/templates/tasks → Create Kanban from template
Conclusion
The electronic tables system in DLE is a powerful tool for managing structured data with:
✅ Flexible structure (6 field types)
✅ Relations between tables (relation, lookup)
✅ AI integration (RAG, vector search)
✅ Real-time updates (WebSocket)
✅ Security (AES-256, access rights)
✅ Performance (indexes, parallel queries)
This is not just Excel, but a full-featured database with convenient interface and AI assistant!
© 2024-2025 Tarabanov Alexander Viktorovich. All rights reserved.
Document version: 1.0.0
Creation date: October 25, 2025
Status: Temporary (for internal use)