# Electronic Tables System in DLE > **Temporary document for internal analysis** --- ## πŸ“‹ Table of Contents 1. [System Overview](#system-overview) 2. [Database Architecture](#database-architecture) 3. [Field Types](#field-types) 4. [Functional Capabilities](#functional-capabilities) 5. [Relations Between Tables](#relations-between-tables) 6. [AI Integration (RAG)](#ai-integration-rag) 7. [API Reference](#api-reference) 8. [Usage Examples](#usage-examples) 9. [Security](#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) ```sql β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ 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 ```sql -- 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 ```json { "type": "text", "options": null } ``` **Usage**: - Names - Descriptions - Email - URL - Any text ### 2. Number **Description**: Numeric field ```json { "type": "number", "options": null } ``` **Usage**: - Prices - Quantities - Ratings - Percentages ### 3. Multiselect **Description**: Multiple value selection from list ```json { "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 ```json { "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) ```json { "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 ```json { "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 ```javascript // 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 ```javascript 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 ```javascript await tablesService.addRow(tableId); // Backend: POST /tables/:id/rows // Automatically indexes in vector store for AI ``` #### Update Cell (Upsert) ```javascript 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 ```javascript await tablesService.deleteRow(rowId); // Backend: DELETE /tables/row/:rowId // Rebuilds vector store (rebuild) ``` #### Delete Column ```javascript 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 ```javascript await tablesService.deleteTable(tableId); // Backend: DELETE /tables/:id // Required: req.session.userAccessLevel?.hasAccess // Cascading deletion of all related data ``` ### 2. Data Filtering #### By Product ```javascript GET /tables/5/rows?product=Premium // Backend filters rows: filtered = rows.filter(r => r.product === 'Premium'); ``` #### By Tags ```javascript 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 ```javascript 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 ```javascript GET /tables/5/rows?multiselect_8=10,11,12 // All selected values must be present ``` ### 3. Placeholder System **Automatic generation**: ```javascript // Function: generatePlaceholder(name, existingPlaceholders) "Customer Name" β†’ "customer_name" "Email" β†’ "email" "Email" (2nd time) β†’ "email_1" "123" β†’ "column" (fallback) "Price-$" β†’ "price" ``` **Transliteration**: ```javascript const cyrillicToLatinMap = { Π°: 'a', Π±: 'b', Π²: 'v', Π³: 'g', Π΄: 'd', Π΅: 'e', Ρ‘: 'e', ΠΆ: 'zh', Π·: 'z', ΠΈ: 'i', // ... full map }; ``` **Usage**: ```javascript // API access to data via placeholder GET /tables/5/data?fields=email,phone,customer_name ``` ### 4. Row Order ```javascript // 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) ```javascript // 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 ```javascript // 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**: ```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 **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**: ```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 (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**: 1. Select `product = "Laptop"` (relation to product) 2. `price` automatically substituted from `Products.price` 3. If product price changes, lookup updates ### API for Working with Relations ```javascript // 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**: ```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 (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**: ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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**: ```json [ { "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**: ```json { "name": "Contacts", "description": "Customer database", "isRagSourceId": 2 } ``` **Response**: Created table object #### GET /tables/:id Get table structure and data **Response**: ```json { "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**: ```json { "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**: ```json { "name": "Email", "type": "text", "order": 2, "purpose": "contact" } ``` #### PATCH /tables/column/:columnId Update column **Request**: ```json { "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**: ```json { "order": [ { "rowId": 100, "order": 0 }, { "rowId": 101, "order": 1 } ] } ``` ### Cells #### POST /tables/cell Create or update cell value (upsert) **Request**: ```json { "row_id": 100, "column_id": 5, "value": "new@email.com" } ``` **Logic**: ```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() ``` ### 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**: ```json { "success": true, "count": 150 } ``` ### Relations #### GET /tables/:tableId/row/:rowId/relations Get all row relations **Response**: ```json [ { "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**: ```json { "column_id": 12, "to_table_id": 5, "to_row_id": 45 } ``` **Multiple relations** (multiselect): ```json { "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**: ```json [ { "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**: ```json [ { "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 ```javascript const table = await tablesService.createTable({ name: "FAQ", description: "Frequently asked questions for AI", isRagSourceId: 2 // RAG source }); ``` #### Add Columns ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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**: ```javascript // 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**: ```sql -- 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 ```javascript // 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 ```javascript // 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**: ```javascript // βœ… 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 ```javascript // 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) ```sql -- 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 ```javascript // 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 ```javascript // 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 ```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); -- Result: fast filtering by relations ``` #### 3. UNIQUE Constraint ```sql 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 ```javascript // ❌ 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 ```javascript // 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 1. **No pagination**: All rows loaded at once - For large tables (>1000 rows) may be slow 2. **No formulas**: Cannot create calculated fields - Workaround: use lookup 3. **No grouping**: Cannot group rows - Workaround: filtering on frontend 4. **No change history**: Not tracked who and when changed - Can add audit trail 5. **Limited sorting**: Only through order field - No column sorting on backend ### Possible Improvements ```javascript // 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)