mirror of
https://github.com/zadam/trilium.git
synced 2026-07-10 22:32:17 +02:00
feat(docs): add some more documentation around features that previously weren't documented
This commit is contained in:
511
docs/Developer Guide/!!!meta.json
vendored
511
docs/Developer Guide/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.97.2",
|
||||
"appVersion": "0.98.0",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
@@ -19,6 +19,503 @@
|
||||
"attachments": [],
|
||||
"dirFileName": "Developer Guide",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "YNX9NN659vF2",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2"
|
||||
],
|
||||
"title": "Architecture",
|
||||
"notePosition": 80,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"attachments": [],
|
||||
"dirFileName": "Architecture",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "yfKVWBgPMQj4",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"yfKVWBgPMQj4"
|
||||
],
|
||||
"title": "API-Architecture",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "1HpEf2wiYVmV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "9GSipWxTZ81I",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "API-Architecture.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "9GSipWxTZ81I",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"9GSipWxTZ81I"
|
||||
],
|
||||
"title": "Entity-System",
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "1HpEf2wiYVmV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Entity-System.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "QR0jtNmTvYqx",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"QR0jtNmTvYqx"
|
||||
],
|
||||
"title": "Monorepo-Structure",
|
||||
"notePosition": 30,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Monorepo-Structure.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "KQT5Xda5Czcl",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"KQT5Xda5Czcl"
|
||||
],
|
||||
"title": "README",
|
||||
"notePosition": 40,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "1HpEf2wiYVmV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "9GSipWxTZ81I",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "NZV7rSwYe0dV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "yfKVWBgPMQj4",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "QR0jtNmTvYqx",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "README.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "1HpEf2wiYVmV",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"1HpEf2wiYVmV"
|
||||
],
|
||||
"title": "Three-Layer-Cache-System",
|
||||
"notePosition": 50,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "9GSipWxTZ81I",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Three-Layer-Cache-System.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "NZV7rSwYe0dV",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"NZV7rSwYe0dV"
|
||||
],
|
||||
"title": "Widget-Based-UI-Architecture",
|
||||
"notePosition": 60,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Widget-Based-UI-Architecture.md",
|
||||
"attachments": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "TfoaMQkD75JI",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI"
|
||||
],
|
||||
"title": "API Documentation",
|
||||
"notePosition": 90,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"attachments": [],
|
||||
"dirFileName": "API Documentation",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "tvrq35NqaWce",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"tvrq35NqaWce"
|
||||
],
|
||||
"title": "API Client Libraries",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "oB5PbEHAzG9t",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "eDWmn0B42ALV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "RqCs8jiZGB1g",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "API Client Libraries.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "oB5PbEHAzG9t",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"oB5PbEHAzG9t"
|
||||
],
|
||||
"title": "ETAPI Complete Guide",
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "ETAPI Complete Guide.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "4sxya4t9P8AA",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"4sxya4t9P8AA"
|
||||
],
|
||||
"title": "Internal API Reference",
|
||||
"notePosition": 30,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "oB5PbEHAzG9t",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "RqCs8jiZGB1g",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "eDWmn0B42ALV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Internal API Reference.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "RqCs8jiZGB1g",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"RqCs8jiZGB1g"
|
||||
],
|
||||
"title": "Script API Cookbook",
|
||||
"notePosition": 40,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Script API Cookbook.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "eDWmn0B42ALV",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"eDWmn0B42ALV"
|
||||
],
|
||||
"title": "WebSocket API",
|
||||
"notePosition": 50,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "4sxya4t9P8AA",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "oB5PbEHAzG9t",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "tvrq35NqaWce",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "WebSocket API.md",
|
||||
"attachments": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "bPMgREWrBYg4",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4"
|
||||
],
|
||||
"title": "Plugin Development",
|
||||
"notePosition": 100,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"attachments": [],
|
||||
"dirFileName": "Plugin Development",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "bZEM5pnOzTVt",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"bZEM5pnOzTVt"
|
||||
],
|
||||
"title": "Backend Script Development",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Backend Script Development.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "fI5sL6zd3n9K",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"fI5sL6zd3n9K"
|
||||
],
|
||||
"title": "Custom Note Type Development",
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Custom Note Type Development.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "gRXGO3vGuey9",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"gRXGO3vGuey9"
|
||||
],
|
||||
"title": "Custom Widget Development Guide",
|
||||
"notePosition": 30,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Custom Widget Development Guid.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "3ZeRzd0e7Btc",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"3ZeRzd0e7Btc"
|
||||
],
|
||||
"title": "Frontend Script Development",
|
||||
"notePosition": 40,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Frontend Script Development.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "dZnOJuAVLKxt",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"dZnOJuAVLKxt"
|
||||
],
|
||||
"title": "Theme Development Guide",
|
||||
"notePosition": 50,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Theme Development Guide.md",
|
||||
"attachments": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "T2W7WCZrYZBU",
|
||||
@@ -27,7 +524,7 @@
|
||||
"T2W7WCZrYZBU"
|
||||
],
|
||||
"title": "Environment Setup",
|
||||
"notePosition": 50,
|
||||
"notePosition": 130,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -53,7 +550,7 @@
|
||||
"cxfTSHIUQtt2"
|
||||
],
|
||||
"title": "Project Structure",
|
||||
"notePosition": 190,
|
||||
"notePosition": 270,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -139,7 +636,7 @@
|
||||
"YjerxU7Aii8X"
|
||||
],
|
||||
"title": "Troubleshooting",
|
||||
"notePosition": 200,
|
||||
"notePosition": 280,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -186,7 +683,7 @@
|
||||
"wbVIolLKDhe2"
|
||||
],
|
||||
"title": "Development and architecture",
|
||||
"notePosition": 220,
|
||||
"notePosition": 300,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -1728,7 +2225,7 @@
|
||||
"VHhyVRYK43gI"
|
||||
],
|
||||
"title": "Building and deployment",
|
||||
"notePosition": 230,
|
||||
"notePosition": 310,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -1767,7 +2264,7 @@
|
||||
"ibAPHul7Efvr"
|
||||
],
|
||||
"title": "Old documentation",
|
||||
"notePosition": 260,
|
||||
"notePosition": 340,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
|
||||
2277
docs/Developer Guide/Developer Guide/API Documentation/API Client Libraries.md
vendored
Normal file
2277
docs/Developer Guide/Developer Guide/API Documentation/API Client Libraries.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1798
docs/Developer Guide/Developer Guide/API Documentation/ETAPI Complete Guide.md
vendored
Normal file
1798
docs/Developer Guide/Developer Guide/API Documentation/ETAPI Complete Guide.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1926
docs/Developer Guide/Developer Guide/API Documentation/Internal API Reference.md
vendored
Normal file
1926
docs/Developer Guide/Developer Guide/API Documentation/Internal API Reference.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1845
docs/Developer Guide/Developer Guide/API Documentation/Script API Cookbook.md
vendored
Normal file
1845
docs/Developer Guide/Developer Guide/API Documentation/Script API Cookbook.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1795
docs/Developer Guide/Developer Guide/API Documentation/WebSocket API.md
vendored
Normal file
1795
docs/Developer Guide/Developer Guide/API Documentation/WebSocket API.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
900
docs/Developer Guide/Developer Guide/Architecture/API-Architecture.md
vendored
Normal file
900
docs/Developer Guide/Developer Guide/Architecture/API-Architecture.md
vendored
Normal file
@@ -0,0 +1,900 @@
|
||||
# API-Architecture
|
||||
## API Architecture
|
||||
|
||||
Trilium provides multiple API layers for different use cases: Internal API for frontend-backend communication, ETAPI for external integrations, and WebSocket for real-time synchronization. This document details each API layer's design, usage, and best practices.
|
||||
|
||||
## API Layers Overview
|
||||
|
||||
```
|
||||
graph TB
|
||||
subgraph "Client Applications"
|
||||
WebApp[Web Application]
|
||||
Desktop[Desktop App]
|
||||
Mobile[Mobile App]
|
||||
External[External Apps]
|
||||
Scripts[User Scripts]
|
||||
end
|
||||
|
||||
subgraph "API Layers"
|
||||
Internal[Internal API<br/>REST + WebSocket]
|
||||
ETAPI[ETAPI<br/>External API]
|
||||
WS[WebSocket<br/>Real-time Sync]
|
||||
end
|
||||
|
||||
subgraph "Backend Services"
|
||||
Routes[Route Handlers]
|
||||
Services[Business Logic]
|
||||
Becca[Becca Cache]
|
||||
DB[(Database)]
|
||||
end
|
||||
|
||||
WebApp --> Internal
|
||||
Desktop --> Internal
|
||||
Mobile --> Internal
|
||||
External --> ETAPI
|
||||
Scripts --> ETAPI
|
||||
|
||||
Internal --> Routes
|
||||
ETAPI --> Routes
|
||||
WS --> Services
|
||||
|
||||
Routes --> Services
|
||||
Services --> Becca
|
||||
Becca --> DB
|
||||
|
||||
style Internal fill:#e3f2fd
|
||||
style ETAPI fill:#fff3e0
|
||||
style WS fill:#f3e5f5
|
||||
```
|
||||
|
||||
## Internal API
|
||||
|
||||
**Location**: `/apps/server/src/routes/api/`
|
||||
|
||||
The Internal API handles communication between Trilium's frontend and backend, providing full access to application functionality.
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// Route structure
|
||||
/api/
|
||||
├── notes.ts // Note operations
|
||||
├── branches.ts // Branch management
|
||||
├── attributes.ts // Attribute operations
|
||||
├── tree.ts // Tree structure
|
||||
├── search.ts // Search functionality
|
||||
├── sync.ts // Synchronization
|
||||
├── options.ts // Configuration
|
||||
└── special.ts // Special operations
|
||||
```
|
||||
|
||||
### Request/Response Pattern
|
||||
|
||||
```typescript
|
||||
// Typical API endpoint structure
|
||||
router.get('/notes/:noteId', (req, res) => {
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).json({
|
||||
error: 'Note not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json(note.getPojo());
|
||||
});
|
||||
|
||||
router.put('/notes/:noteId', (req, res) => {
|
||||
const note = becca.getNoteOrThrow(req.params.noteId);
|
||||
|
||||
note.title = req.body.title;
|
||||
note.content = req.body.content;
|
||||
note.save();
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
### Key Endpoints
|
||||
|
||||
#### Note Operations
|
||||
|
||||
```typescript
|
||||
// Get note with content
|
||||
GET /api/notes/:noteId
|
||||
Response: {
|
||||
noteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
content: string,
|
||||
dateCreated: string,
|
||||
dateModified: string
|
||||
}
|
||||
|
||||
// Update note
|
||||
PUT /api/notes/:noteId
|
||||
Body: {
|
||||
title?: string,
|
||||
content?: string,
|
||||
type?: string,
|
||||
mime?: string
|
||||
}
|
||||
|
||||
// Create note
|
||||
POST /api/notes/:parentNoteId/children
|
||||
Body: {
|
||||
title: string,
|
||||
type: string,
|
||||
content?: string,
|
||||
position?: number
|
||||
}
|
||||
|
||||
// Delete note
|
||||
DELETE /api/notes/:noteId
|
||||
```
|
||||
|
||||
#### Tree Operations
|
||||
|
||||
```typescript
|
||||
// Get tree structure
|
||||
GET /api/tree
|
||||
Query: {
|
||||
subTreeNoteId?: string,
|
||||
includeAttributes?: boolean
|
||||
}
|
||||
Response: {
|
||||
notes: FNoteRow[],
|
||||
branches: FBranchRow[],
|
||||
attributes: FAttributeRow[]
|
||||
}
|
||||
|
||||
// Move branch
|
||||
PUT /api/branches/:branchId/move
|
||||
Body: {
|
||||
parentNoteId: string,
|
||||
position: number
|
||||
}
|
||||
```
|
||||
|
||||
#### Search Operations
|
||||
|
||||
```typescript
|
||||
// Execute search
|
||||
GET /api/search
|
||||
Query: {
|
||||
query: string,
|
||||
fastSearch?: boolean,
|
||||
includeArchivedNotes?: boolean,
|
||||
ancestorNoteId?: string
|
||||
}
|
||||
Response: {
|
||||
results: Array<{
|
||||
noteId: string,
|
||||
title: string,
|
||||
path: string,
|
||||
score: number
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication & Security
|
||||
|
||||
```typescript
|
||||
// CSRF protection
|
||||
app.use(csrfMiddleware);
|
||||
|
||||
// Session authentication
|
||||
router.use((req, res, next) => {
|
||||
if (!req.session.loggedIn) {
|
||||
return res.status(401).json({
|
||||
error: 'Not authenticated'
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Protected note access
|
||||
router.get('/notes/:noteId', (req, res) => {
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
|
||||
if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
|
||||
return res.status(403).json({
|
||||
error: 'Protected session required'
|
||||
});
|
||||
}
|
||||
|
||||
res.json(note.getPojo());
|
||||
});
|
||||
```
|
||||
|
||||
## ETAPI (External API)
|
||||
|
||||
**Location**: `/apps/server/src/etapi/`
|
||||
|
||||
ETAPI provides a stable, versioned API for external applications and scripts to interact with Trilium.
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// ETAPI structure
|
||||
/etapi/
|
||||
├── etapi.openapi.yaml // OpenAPI specification
|
||||
├── auth.ts // Authentication
|
||||
├── notes.ts // Note endpoints
|
||||
├── branches.ts // Branch endpoints
|
||||
├── attributes.ts // Attribute endpoints
|
||||
├── attachments.ts // Attachment endpoints
|
||||
└── special_notes.ts // Special note operations
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
ETAPI uses token-based authentication:
|
||||
|
||||
```typescript
|
||||
// Creating ETAPI token
|
||||
POST /etapi/auth/login
|
||||
Body: {
|
||||
username: string,
|
||||
password: string
|
||||
}
|
||||
Response: {
|
||||
authToken: string
|
||||
}
|
||||
|
||||
// Using token in requests
|
||||
GET /etapi/notes/:noteId
|
||||
Headers: {
|
||||
Authorization: "authToken"
|
||||
}
|
||||
```
|
||||
|
||||
### Key Endpoints
|
||||
|
||||
#### Note CRUD Operations
|
||||
|
||||
```typescript
|
||||
// Create note
|
||||
POST /etapi/notes
|
||||
Body: {
|
||||
noteId?: string,
|
||||
parentNoteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
content?: string,
|
||||
position?: number
|
||||
}
|
||||
|
||||
// Get note
|
||||
GET /etapi/notes/:noteId
|
||||
Response: {
|
||||
noteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
mime: string,
|
||||
isProtected: boolean,
|
||||
attributes: Array<{
|
||||
attributeId: string,
|
||||
type: string,
|
||||
name: string,
|
||||
value: string
|
||||
}>,
|
||||
parentNoteIds: string[],
|
||||
childNoteIds: string[],
|
||||
dateCreated: string,
|
||||
dateModified: string
|
||||
}
|
||||
|
||||
// Update note content
|
||||
PUT /etapi/notes/:noteId/content
|
||||
Body: string | Buffer
|
||||
Headers: {
|
||||
"Content-Type": mime-type
|
||||
}
|
||||
|
||||
// Delete note
|
||||
DELETE /etapi/notes/:noteId
|
||||
```
|
||||
|
||||
#### Attribute Management
|
||||
|
||||
```typescript
|
||||
// Create attribute
|
||||
POST /etapi/attributes
|
||||
Body: {
|
||||
noteId: string,
|
||||
type: 'label' | 'relation',
|
||||
name: string,
|
||||
value: string,
|
||||
isInheritable?: boolean
|
||||
}
|
||||
|
||||
// Update attribute
|
||||
PATCH /etapi/attributes/:attributeId
|
||||
Body: {
|
||||
value?: string,
|
||||
isInheritable?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
#### Search
|
||||
|
||||
```typescript
|
||||
// Search notes
|
||||
GET /etapi/notes/search
|
||||
Query: {
|
||||
search: string,
|
||||
limit?: number,
|
||||
orderBy?: string,
|
||||
orderDirection?: 'asc' | 'desc'
|
||||
}
|
||||
Response: {
|
||||
results: Array<{
|
||||
noteId: string,
|
||||
title: string,
|
||||
// Other note properties
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
### Client Libraries
|
||||
|
||||
```javascript
|
||||
// JavaScript client example
|
||||
class EtapiClient {
|
||||
constructor(serverUrl, authToken) {
|
||||
this.serverUrl = serverUrl;
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
async getNote(noteId) {
|
||||
const response = await fetch(
|
||||
`${this.serverUrl}/etapi/notes/${noteId}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': this.authToken
|
||||
}
|
||||
}
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createNote(parentNoteId, title, content) {
|
||||
const response = await fetch(
|
||||
`${this.serverUrl}/etapi/notes`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': this.authToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parentNoteId,
|
||||
title,
|
||||
type: 'text',
|
||||
content
|
||||
})
|
||||
}
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python Client Example
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
class TriliumETAPI:
|
||||
def __init__(self, server_url, auth_token):
|
||||
self.server_url = server_url
|
||||
self.auth_token = auth_token
|
||||
self.headers = {'Authorization': auth_token}
|
||||
|
||||
def get_note(self, note_id):
|
||||
response = requests.get(
|
||||
f"{self.server_url}/etapi/notes/{note_id}",
|
||||
headers=self.headers
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def create_note(self, parent_note_id, title, content=""):
|
||||
response = requests.post(
|
||||
f"{self.server_url}/etapi/notes",
|
||||
headers=self.headers,
|
||||
json={
|
||||
'parentNoteId': parent_note_id,
|
||||
'title': title,
|
||||
'type': 'text',
|
||||
'content': content
|
||||
}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def search_notes(self, query):
|
||||
response = requests.get(
|
||||
f"{self.server_url}/etapi/notes/search",
|
||||
headers=self.headers,
|
||||
params={'search': query}
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## WebSocket Real-time Synchronization
|
||||
|
||||
**Location**: `/apps/server/src/services/ws.ts`
|
||||
|
||||
WebSocket connections provide real-time updates and synchronization between clients.
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// WebSocket message types
|
||||
interface WSMessage {
|
||||
type: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
// Common message types
|
||||
type MessageType =
|
||||
| 'entity-changes' // Entity updates
|
||||
| 'sync' // Sync events
|
||||
| 'note-content-change' // Content updates
|
||||
| 'refresh-tree' // Tree structure changes
|
||||
| 'options-changed' // Configuration updates
|
||||
```
|
||||
|
||||
### Connection Management
|
||||
|
||||
```typescript
|
||||
// Client connection
|
||||
const ws = new WebSocket('wss://server/ws');
|
||||
|
||||
ws.on('open', () => {
|
||||
// Authenticate
|
||||
ws.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
token: sessionToken
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
handleWSMessage(message);
|
||||
});
|
||||
|
||||
// Server-side handling
|
||||
import WebSocket from 'ws';
|
||||
|
||||
const wss = new WebSocket.Server({ server });
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const session = parseSession(req);
|
||||
|
||||
if (!session.authenticated) {
|
||||
ws.close(1008, 'Not authenticated');
|
||||
return;
|
||||
}
|
||||
|
||||
clients.add(ws);
|
||||
|
||||
ws.on('message', (message) => {
|
||||
handleClientMessage(ws, message);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
clients.delete(ws);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Message Broadcasting
|
||||
|
||||
```typescript
|
||||
// Broadcast entity changes
|
||||
function broadcastEntityChanges(changes: EntityChange[]) {
|
||||
const message = {
|
||||
type: 'entity-changes',
|
||||
data: changes
|
||||
};
|
||||
|
||||
for (const client of clients) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Targeted messages
|
||||
function sendToClient(clientId: string, message: WSMessage) {
|
||||
const client = clients.get(clientId);
|
||||
if (client?.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Real-time Sync Protocol
|
||||
|
||||
```typescript
|
||||
// Entity change notification
|
||||
{
|
||||
type: 'entity-changes',
|
||||
data: [
|
||||
{
|
||||
entityName: 'notes',
|
||||
entityId: 'noteId123',
|
||||
action: 'update',
|
||||
entity: { /* note data */ }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Sync pull request
|
||||
{
|
||||
type: 'sync-pull',
|
||||
data: {
|
||||
lastSyncId: 12345
|
||||
}
|
||||
}
|
||||
|
||||
// Sync push
|
||||
{
|
||||
type: 'sync-push',
|
||||
data: {
|
||||
entities: [ /* changed entities */ ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client-side Handling
|
||||
|
||||
```typescript
|
||||
// Froca WebSocket integration
|
||||
class WSClient {
|
||||
constructor() {
|
||||
this.ws = null;
|
||||
this.reconnectTimeout = null;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.getWSUrl());
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleMessage(message);
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
// Reconnect with exponential backoff
|
||||
this.scheduleReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
handleMessage(message: WSMessage) {
|
||||
switch (message.type) {
|
||||
case 'entity-changes':
|
||||
this.handleEntityChanges(message.data);
|
||||
break;
|
||||
case 'refresh-tree':
|
||||
froca.loadInitialTree();
|
||||
break;
|
||||
case 'note-content-change':
|
||||
this.handleContentChange(message.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleEntityChanges(changes: EntityChange[]) {
|
||||
for (const change of changes) {
|
||||
if (change.entityName === 'notes') {
|
||||
froca.reloadNotes([change.entityId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Security
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
```typescript
|
||||
// 1. Session-based (Internal API)
|
||||
app.use(session({
|
||||
secret: config.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
|
||||
// 2. Token-based (ETAPI)
|
||||
router.use('/etapi', (req, res, next) => {
|
||||
const token = req.headers.authorization;
|
||||
|
||||
const etapiToken = becca.getEtapiToken(token);
|
||||
if (!etapiToken || etapiToken.isExpired()) {
|
||||
return res.status(401).json({
|
||||
error: 'Invalid or expired token'
|
||||
});
|
||||
}
|
||||
|
||||
req.etapiToken = etapiToken;
|
||||
next();
|
||||
});
|
||||
|
||||
// 3. WebSocket authentication
|
||||
ws.on('connection', (socket) => {
|
||||
socket.on('auth', (token) => {
|
||||
if (!validateToken(token)) {
|
||||
socket.close(1008, 'Invalid token');
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
// Global rate limit
|
||||
const globalLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1000 // limit each IP to 1000 requests per windowMs
|
||||
});
|
||||
|
||||
// Strict limit for authentication
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5,
|
||||
message: 'Too many authentication attempts'
|
||||
});
|
||||
|
||||
app.use('/api', globalLimiter);
|
||||
app.use('/api/auth', authLimiter);
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```typescript
|
||||
import { body, validationResult } from 'express-validator';
|
||||
|
||||
router.post('/api/notes',
|
||||
body('title').isString().isLength({ min: 1, max: 1000 }),
|
||||
body('type').isIn(['text', 'code', 'file', 'image']),
|
||||
body('content').optional().isString(),
|
||||
(req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
// Process valid input
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Caching Strategies
|
||||
|
||||
```typescript
|
||||
// Response caching
|
||||
const cache = new Map();
|
||||
|
||||
router.get('/api/notes/:noteId', (req, res) => {
|
||||
const cacheKey = `note:${req.params.noteId}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
return res.json(cached.data);
|
||||
}
|
||||
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
const data = note.getPojo();
|
||||
|
||||
cache.set(cacheKey, {
|
||||
data,
|
||||
expires: Date.now() + 60000 // 1 minute
|
||||
});
|
||||
|
||||
res.json(data);
|
||||
});
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Batch API endpoint
|
||||
router.post('/api/batch', async (req, res) => {
|
||||
const operations = req.body.operations;
|
||||
const results = [];
|
||||
|
||||
await sql.transactional(async () => {
|
||||
for (const op of operations) {
|
||||
const result = await executeOperation(op);
|
||||
results.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ results });
|
||||
});
|
||||
|
||||
// Client batch usage
|
||||
const batch = [
|
||||
{ method: 'PUT', path: '/notes/1', body: { title: 'Note 1' }},
|
||||
{ method: 'PUT', path: '/notes/2', body: { title: 'Note 2' }},
|
||||
{ method: 'POST', path: '/notes/3/attributes', body: { type: 'label', name: 'todo' }}
|
||||
];
|
||||
|
||||
await api.post('/batch', { operations: batch });
|
||||
```
|
||||
|
||||
### Streaming Responses
|
||||
|
||||
```typescript
|
||||
// Stream large data
|
||||
router.get('/api/export', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-ndjson',
|
||||
'Transfer-Encoding': 'chunked'
|
||||
});
|
||||
|
||||
const noteStream = createNoteExportStream();
|
||||
|
||||
noteStream.on('data', (note) => {
|
||||
res.write(JSON.stringify(note) + '\n');
|
||||
});
|
||||
|
||||
noteStream.on('end', () => {
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Standard Error Responses
|
||||
|
||||
```typescript
|
||||
// Error response format
|
||||
interface ErrorResponse {
|
||||
error: string;
|
||||
code?: string;
|
||||
details?: any;
|
||||
}
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
console.error('API Error:', err);
|
||||
|
||||
if (err instanceof NotFoundError) {
|
||||
return res.status(404).json({
|
||||
error: err.message,
|
||||
code: 'NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
if (err instanceof ValidationError) {
|
||||
return res.status(400).json({
|
||||
error: err.message,
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: err.details
|
||||
});
|
||||
}
|
||||
|
||||
// Generic error
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
code: 'INTERNAL_ERROR'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
### OpenAPI/Swagger
|
||||
|
||||
```yaml
|
||||
# etapi.openapi.yaml
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Trilium ETAPI
|
||||
version: 1.0.0
|
||||
description: External API for Trilium Notes
|
||||
|
||||
paths:
|
||||
/etapi/notes/{noteId}:
|
||||
get:
|
||||
summary: Get note by ID
|
||||
parameters:
|
||||
- name: noteId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Note found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Note'
|
||||
404:
|
||||
description: Note not found
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Note:
|
||||
type: object
|
||||
properties:
|
||||
noteId:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [text, code, file, image]
|
||||
```
|
||||
|
||||
### API Testing
|
||||
|
||||
```typescript
|
||||
// API test example
|
||||
describe('Notes API', () => {
|
||||
it('should create a note', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/notes/root/children')
|
||||
.send({
|
||||
title: 'Test Note',
|
||||
type: 'text',
|
||||
content: 'Test content'
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('noteId');
|
||||
expect(response.body.title).toBe('Test Note');
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/notes/invalid')
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### API Design
|
||||
|
||||
1. **RESTful conventions**: Use appropriate HTTP methods and status codes
|
||||
2. **Consistent naming**: Use camelCase for JSON properties
|
||||
3. **Versioning**: Version the API to maintain compatibility
|
||||
4. **Documentation**: Keep OpenAPI spec up to date
|
||||
|
||||
### Security
|
||||
|
||||
1. **Authentication**: Always verify user identity
|
||||
2. **Authorization**: Check permissions for each operation
|
||||
3. **Validation**: Validate all input data
|
||||
4. **Rate limiting**: Prevent abuse with appropriate limits
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Pagination**: Limit response sizes with pagination
|
||||
2. **Caching**: Cache frequently accessed data
|
||||
3. **Batch operations**: Support bulk operations
|
||||
4. **Async processing**: Use queues for long-running tasks
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture
|
||||
* [Entity System](Entity-System.md) - Data model
|
||||
* [ETAPI Reference](#root/YCSbHgSqH9PA) - OpenAPI specification
|
||||
614
docs/Developer Guide/Developer Guide/Architecture/Entity-System.md
vendored
Normal file
614
docs/Developer Guide/Developer Guide/Architecture/Entity-System.md
vendored
Normal file
@@ -0,0 +1,614 @@
|
||||
# Entity-System
|
||||
## Entity System Architecture
|
||||
|
||||
The Entity System forms the core data model of Trilium Notes, providing a flexible and powerful structure for organizing information. This document details the entities, their relationships, and usage patterns.
|
||||
|
||||
## Core Entities Overview
|
||||
|
||||
```
|
||||
erDiagram
|
||||
Note ||--o{ Branch : "parent-child"
|
||||
Note ||--o{ Attribute : "has"
|
||||
Note ||--o{ Revision : "history"
|
||||
Note ||--o{ Attachment : "contains"
|
||||
Attachment ||--|| Blob : "stores in"
|
||||
Revision ||--|| Blob : "stores in"
|
||||
Note }o--o{ Note : "relates via Attribute"
|
||||
|
||||
Note {
|
||||
string noteId PK
|
||||
string title
|
||||
string type
|
||||
string content
|
||||
boolean isProtected
|
||||
string dateCreated
|
||||
string dateModified
|
||||
}
|
||||
|
||||
Branch {
|
||||
string branchId PK
|
||||
string noteId FK
|
||||
string parentNoteId FK
|
||||
integer notePosition
|
||||
string prefix
|
||||
boolean isExpanded
|
||||
}
|
||||
|
||||
Attribute {
|
||||
string attributeId PK
|
||||
string noteId FK
|
||||
string type "label or relation"
|
||||
string name
|
||||
string value
|
||||
integer position
|
||||
boolean isInheritable
|
||||
}
|
||||
|
||||
Revision {
|
||||
string revisionId PK
|
||||
string noteId FK
|
||||
string title
|
||||
string type
|
||||
boolean isProtected
|
||||
string dateCreated
|
||||
}
|
||||
|
||||
Attachment {
|
||||
string attachmentId PK
|
||||
string ownerId FK
|
||||
string role
|
||||
string mime
|
||||
string title
|
||||
string blobId FK
|
||||
}
|
||||
|
||||
Blob {
|
||||
string blobId PK
|
||||
binary content
|
||||
string dateModified
|
||||
}
|
||||
|
||||
Option {
|
||||
string name PK
|
||||
string value
|
||||
boolean isSynced
|
||||
}
|
||||
```
|
||||
|
||||
## Entity Definitions
|
||||
|
||||
### BNote - Notes with Content and Metadata
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/bnote.ts`
|
||||
|
||||
Notes are the fundamental unit of information in Trilium. Each note can contain different types of content and maintain relationships with other notes.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BNote {
|
||||
noteId: string; // Unique identifier
|
||||
title: string; // Display title
|
||||
type: string; // Content type (text, code, file, etc.)
|
||||
mime: string; // MIME type for content
|
||||
isProtected: boolean; // Encryption flag
|
||||
dateCreated: string; // Creation timestamp
|
||||
dateModified: string; // Last modification
|
||||
utcDateCreated: string; // UTC creation
|
||||
utcDateModified: string; // UTC modification
|
||||
|
||||
// Relationships
|
||||
parentBranches: BBranch[]; // Parent connections
|
||||
children: BBranch[]; // Child connections
|
||||
attributes: BAttribute[]; // Metadata
|
||||
|
||||
// Content
|
||||
content?: string | Buffer; // Note content (lazy loaded)
|
||||
|
||||
// Computed
|
||||
isDecrypted: boolean; // Decryption status
|
||||
}
|
||||
```
|
||||
|
||||
#### Note Types
|
||||
|
||||
* **text**: Rich text content with HTML formatting
|
||||
* **code**: Source code with syntax highlighting
|
||||
* **file**: Binary file attachment
|
||||
* **image**: Image with preview capabilities
|
||||
* **search**: Saved search query
|
||||
* **book**: Container for hierarchical documentation
|
||||
* **relationMap**: Visual relationship diagram
|
||||
* **canvas**: Drawing canvas (Excalidraw)
|
||||
* **mermaid**: Mermaid diagram
|
||||
* **mindMap**: Mind mapping visualization
|
||||
* **webView**: Embedded web content
|
||||
* **noteMap**: Tree visualization
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```typescript
|
||||
// Create a new note
|
||||
const note = new BNote({
|
||||
noteId: generateNoteId(),
|
||||
title: "My Note",
|
||||
type: "text",
|
||||
mime: "text/html",
|
||||
content: "<p>Note content</p>"
|
||||
});
|
||||
note.save();
|
||||
|
||||
// Get note with content
|
||||
const note = becca.getNote(noteId);
|
||||
await note.loadContent();
|
||||
|
||||
// Update note
|
||||
note.title = "Updated Title";
|
||||
note.save();
|
||||
|
||||
// Protect note
|
||||
note.isProtected = true;
|
||||
note.encrypt();
|
||||
note.save();
|
||||
```
|
||||
|
||||
### BBranch - Hierarchical Relationships
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/bbranch.ts`
|
||||
|
||||
Branches define the parent-child relationships between notes, allowing a note to have multiple parents (cloning).
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BBranch {
|
||||
branchId: string; // Unique identifier
|
||||
noteId: string; // Child note ID
|
||||
parentNoteId: string; // Parent note ID
|
||||
notePosition: number; // Order among siblings
|
||||
prefix: string; // Optional prefix label
|
||||
isExpanded: boolean; // Tree UI state
|
||||
|
||||
// Computed
|
||||
childNote: BNote; // Reference to child
|
||||
parentNote: BNote; // Reference to parent
|
||||
}
|
||||
```
|
||||
|
||||
#### Key Features
|
||||
|
||||
* **Multiple Parents**: Notes can appear in multiple locations
|
||||
* **Ordering**: Explicit positioning among siblings
|
||||
* **Prefixes**: Optional labels for context (e.g., "Chapter 1:")
|
||||
* **UI State**: Expansion state persisted per branch
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```typescript
|
||||
// Create parent-child relationship
|
||||
const branch = new BBranch({
|
||||
noteId: childNote.noteId,
|
||||
parentNoteId: parentNote.noteId,
|
||||
notePosition: 10
|
||||
});
|
||||
branch.save();
|
||||
|
||||
// Clone note to another parent
|
||||
const cloneBranch = childNote.cloneTo(otherParent.noteId);
|
||||
|
||||
// Reorder children
|
||||
parentNote.sortChildren((a, b) =>
|
||||
a.title.localeCompare(b.title)
|
||||
);
|
||||
|
||||
// Add prefix
|
||||
branch.prefix = "Important: ";
|
||||
branch.save();
|
||||
```
|
||||
|
||||
### BAttribute - Key-Value Metadata
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/battribute.ts`
|
||||
|
||||
Attributes provide flexible metadata and relationships between notes.
|
||||
|
||||
#### Types
|
||||
|
||||
1. **Labels**: Key-value pairs for metadata
|
||||
2. **Relations**: References to other notes
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BAttribute {
|
||||
attributeId: string; // Unique identifier
|
||||
noteId: string; // Owning note
|
||||
type: 'label' | 'relation';
|
||||
name: string; // Attribute name
|
||||
value: string; // Value or target noteId
|
||||
position: number; // Display order
|
||||
isInheritable: boolean; // Inherited by children
|
||||
|
||||
// Computed
|
||||
note: BNote; // Owner note
|
||||
targetNote?: BNote; // For relations
|
||||
}
|
||||
```
|
||||
|
||||
#### Common Patterns
|
||||
|
||||
```typescript
|
||||
// Add label
|
||||
note.addLabel("status", "active");
|
||||
note.addLabel("priority", "high");
|
||||
|
||||
// Add relation
|
||||
note.addRelation("template", templateNoteId);
|
||||
note.addRelation("renderNote", renderNoteId);
|
||||
|
||||
// Query by attributes
|
||||
const todos = becca.findAttributes("label", "todoItem");
|
||||
const templates = becca.findAttributes("label", "template");
|
||||
|
||||
// Inheritable attributes
|
||||
note.addLabel("workspace", "project", true); // Children inherit
|
||||
```
|
||||
|
||||
#### System Attributes
|
||||
|
||||
Special attributes with system behavior:
|
||||
|
||||
* `#hidePromotedAttributes`: Hide promoted attributes in UI
|
||||
* `#readOnly`: Prevent note editing
|
||||
* `#autoReadOnlyDisabled`: Disable auto read-only
|
||||
* `#hideChildrenOverview`: Hide children count
|
||||
* `~template`: Note template relation
|
||||
* `~renderNote`: Custom rendering relation
|
||||
|
||||
### BRevision - Version History
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/brevision.ts`
|
||||
|
||||
Revisions provide version history and recovery capabilities.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BRevision {
|
||||
revisionId: string; // Unique identifier
|
||||
noteId: string; // Parent note
|
||||
type: string; // Content type
|
||||
mime: string; // MIME type
|
||||
title: string; // Historical title
|
||||
isProtected: boolean; // Encryption flag
|
||||
dateCreated: string; // Creation time
|
||||
utcDateCreated: string; // UTC time
|
||||
dateModified: string; // Content modification
|
||||
blobId: string; // Content storage
|
||||
|
||||
// Methods
|
||||
getContent(): string | Buffer;
|
||||
restore(): void;
|
||||
}
|
||||
```
|
||||
|
||||
#### Revision Strategy
|
||||
|
||||
* Created automatically on significant changes
|
||||
* Configurable retention period
|
||||
* Day/week/month/year retention rules
|
||||
* Protected note revisions are encrypted
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```typescript
|
||||
// Get note revisions
|
||||
const revisions = note.getRevisions();
|
||||
|
||||
// Restore revision
|
||||
const revision = becca.getRevision(revisionId);
|
||||
revision.restore();
|
||||
|
||||
// Manual revision creation
|
||||
note.saveRevision();
|
||||
|
||||
// Compare revisions
|
||||
const diff = revision1.getContent() !== revision2.getContent();
|
||||
```
|
||||
|
||||
### BOption - Application Configuration
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/boption.ts`
|
||||
|
||||
Options store application and user preferences.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BOption {
|
||||
name: string; // Option key
|
||||
value: string; // Option value
|
||||
isSynced: boolean; // Sync across instances
|
||||
utcDateModified: string; // Last change
|
||||
}
|
||||
```
|
||||
|
||||
#### Common Options
|
||||
|
||||
```typescript
|
||||
// Theme settings
|
||||
setOption("theme", "dark");
|
||||
|
||||
// Protected session timeout
|
||||
setOption("protectedSessionTimeout", "600");
|
||||
|
||||
// Sync settings
|
||||
setOption("syncServerHost", "https://sync.server");
|
||||
|
||||
// Note settings
|
||||
setOption("defaultNoteType", "text");
|
||||
```
|
||||
|
||||
### BAttachment - File Attachments
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/battachment.ts`
|
||||
|
||||
Attachments link binary content to notes.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BAttachment {
|
||||
attachmentId: string; // Unique identifier
|
||||
ownerId: string; // Parent note ID
|
||||
role: string; // Attachment role
|
||||
mime: string; // MIME type
|
||||
title: string; // Display title
|
||||
blobId: string; // Content reference
|
||||
utcDateScheduledForDeletion: string;
|
||||
|
||||
// Methods
|
||||
getContent(): Buffer;
|
||||
getBlob(): BBlob;
|
||||
}
|
||||
```
|
||||
|
||||
#### Usage Patterns
|
||||
|
||||
```typescript
|
||||
// Add attachment to note
|
||||
const attachment = note.addAttachment({
|
||||
role: "file",
|
||||
mime: "application/pdf",
|
||||
title: "document.pdf",
|
||||
content: buffer
|
||||
});
|
||||
|
||||
// Get attachments
|
||||
const attachments = note.getAttachments();
|
||||
|
||||
// Download attachment
|
||||
const content = attachment.getContent();
|
||||
```
|
||||
|
||||
## Entity Relationships
|
||||
|
||||
### Parent-Child Hierarchy
|
||||
|
||||
```typescript
|
||||
// Single parent
|
||||
childNote.setParent(parentNote.noteId);
|
||||
|
||||
// Multiple parents (cloning)
|
||||
childNote.cloneTo(parent1.noteId);
|
||||
childNote.cloneTo(parent2.noteId);
|
||||
|
||||
// Get parents
|
||||
const parents = childNote.getParentNotes();
|
||||
|
||||
// Get children
|
||||
const children = parentNote.getChildNotes();
|
||||
|
||||
// Get subtree
|
||||
const subtree = parentNote.getSubtreeNotes();
|
||||
```
|
||||
|
||||
### Attribute Relationships
|
||||
|
||||
```typescript
|
||||
// Direct relations
|
||||
note.addRelation("author", authorNote.noteId);
|
||||
|
||||
// Bidirectional relations
|
||||
note1.addRelation("related", note2.noteId);
|
||||
note2.addRelation("related", note1.noteId);
|
||||
|
||||
// Get related notes
|
||||
const related = note.getRelations("related");
|
||||
|
||||
// Get notes relating to this one
|
||||
const targetRelations = note.getTargetRelations();
|
||||
```
|
||||
|
||||
## Entity Lifecycle
|
||||
|
||||
### Creation
|
||||
|
||||
```typescript
|
||||
// Note creation
|
||||
const note = new BNote({
|
||||
noteId: generateNoteId(),
|
||||
title: "New Note",
|
||||
type: "text"
|
||||
});
|
||||
note.save();
|
||||
|
||||
// With parent
|
||||
const child = parentNote.addChild({
|
||||
title: "Child Note",
|
||||
type: "text",
|
||||
content: "Content"
|
||||
});
|
||||
```
|
||||
|
||||
### Updates
|
||||
|
||||
```typescript
|
||||
// Atomic updates
|
||||
note.title = "New Title";
|
||||
note.save();
|
||||
|
||||
// Batch updates
|
||||
sql.transactional(() => {
|
||||
note1.title = "Title 1";
|
||||
note1.save();
|
||||
|
||||
note2.content = "Content 2";
|
||||
note2.save();
|
||||
});
|
||||
```
|
||||
|
||||
### Deletion
|
||||
|
||||
```typescript
|
||||
// Soft delete (move to trash)
|
||||
note.deleteNote();
|
||||
|
||||
// Mark for deletion
|
||||
note.isDeleted = true;
|
||||
note.save();
|
||||
|
||||
// Permanent deletion (after grace period)
|
||||
note.eraseNote();
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
// Note content loaded on demand
|
||||
const note = becca.getNote(noteId); // Metadata only
|
||||
await note.loadContent(); // Load content when needed
|
||||
|
||||
// Revisions loaded on demand
|
||||
const revisions = note.getRevisions(); // Database query
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Efficient bulk loading
|
||||
const notes = becca.getNotes(noteIds);
|
||||
|
||||
// Batch attribute queries
|
||||
const attributes = sql.getRows(`
|
||||
SELECT * FROM attributes
|
||||
WHERE noteId IN (???)
|
||||
AND name = ?
|
||||
`, [noteIds, 'label']);
|
||||
```
|
||||
|
||||
### Indexing
|
||||
|
||||
```typescript
|
||||
// Attribute index for fast lookups
|
||||
const labels = becca.findAttributes("label", "important");
|
||||
|
||||
// Branch index for relationship queries
|
||||
const branch = becca.getBranchFromChildAndParent(childId, parentId);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Entity Creation
|
||||
|
||||
```typescript
|
||||
// Always use transactions for multiple operations
|
||||
sql.transactional(() => {
|
||||
const note = new BNote({...});
|
||||
note.save();
|
||||
|
||||
note.addLabel("status", "draft");
|
||||
note.addRelation("template", templateId);
|
||||
});
|
||||
```
|
||||
|
||||
### Entity Updates
|
||||
|
||||
```typescript
|
||||
// Check existence before update
|
||||
const note = becca.getNote(noteId);
|
||||
if (note) {
|
||||
note.title = "Updated";
|
||||
note.save();
|
||||
}
|
||||
|
||||
// Use proper error handling
|
||||
try {
|
||||
const note = becca.getNoteOrThrow(noteId);
|
||||
note.save();
|
||||
} catch (e) {
|
||||
log.error(`Note ${noteId} not found`);
|
||||
}
|
||||
```
|
||||
|
||||
### Querying
|
||||
|
||||
```typescript
|
||||
// Use indexed queries
|
||||
const attrs = becca.findAttributes("label", "task");
|
||||
|
||||
// Avoid N+1 queries
|
||||
const noteIds = [...];
|
||||
const notes = becca.getNotes(noteIds); // Single batch
|
||||
|
||||
// Use SQL for complex queries
|
||||
const results = sql.getRows(`
|
||||
SELECT n.noteId, n.title, COUNT(b.branchId) as childCount
|
||||
FROM notes n
|
||||
LEFT JOIN branches b ON b.parentNoteId = n.noteId
|
||||
GROUP BY n.noteId
|
||||
`);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Circular References**
|
||||
|
||||
```typescript
|
||||
// Detect cycles before creating branches
|
||||
if (!parentNote.hasAncestor(childNote.noteId)) {
|
||||
childNote.setParent(parentNote.noteId);
|
||||
}
|
||||
```
|
||||
2. **Orphaned Entities**
|
||||
|
||||
```typescript
|
||||
// Find orphaned notes
|
||||
const orphans = sql.getRows(`
|
||||
SELECT noteId FROM notes
|
||||
WHERE noteId != 'root'
|
||||
AND noteId NOT IN (SELECT noteId FROM branches)
|
||||
`);
|
||||
```
|
||||
3. **Attribute Conflicts**
|
||||
|
||||
```typescript
|
||||
// Handle duplicate attributes
|
||||
const existing = note.getAttribute("label", "status");
|
||||
if (existing) {
|
||||
existing.value = "new value";
|
||||
existing.save();
|
||||
} else {
|
||||
note.addLabel("status", "new value");
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture
|
||||
* [Database Schema](#root/eZcnGfMUmic0) - Database structure
|
||||
* [Script API](#root/7Pp4moCrBVzA) - Entity API for scripts
|
||||
612
docs/Developer Guide/Developer Guide/Architecture/Monorepo-Structure.md
vendored
Normal file
612
docs/Developer Guide/Developer Guide/Architecture/Monorepo-Structure.md
vendored
Normal file
@@ -0,0 +1,612 @@
|
||||
# Monorepo-Structure
|
||||
## Monorepo Structure
|
||||
|
||||
Trilium is organized as a TypeScript monorepo using NX, facilitating code sharing, consistent tooling, and efficient build processes. This document provides a comprehensive overview of the project structure, build system, and development workflow.
|
||||
|
||||
## Project Organization
|
||||
|
||||
```
|
||||
TriliumNext/Trilium/
|
||||
├── apps/ # Runnable applications
|
||||
│ ├── client/ # Frontend web application
|
||||
│ ├── server/ # Node.js backend server
|
||||
│ ├── desktop/ # Electron desktop application
|
||||
│ ├── web-clipper/ # Browser extension
|
||||
│ ├── db-compare/ # Database comparison tool
|
||||
│ ├── dump-db/ # Database dump utility
|
||||
│ └── edit-docs/ # Documentation editor
|
||||
├── packages/ # Shared libraries
|
||||
│ ├── commons/ # Shared interfaces and utilities
|
||||
│ ├── ckeditor5/ # Rich text editor
|
||||
│ ├── codemirror/ # Code editor
|
||||
│ ├── highlightjs/ # Syntax highlighting
|
||||
│ ├── ckeditor5-admonition/ # CKEditor plugin
|
||||
│ ├── ckeditor5-footnotes/ # CKEditor plugin
|
||||
│ ├── ckeditor5-math/ # CKEditor plugin
|
||||
│ └── ckeditor5-mermaid/ # CKEditor plugin
|
||||
├── docs/ # Documentation
|
||||
├── nx.json # NX workspace configuration
|
||||
├── package.json # Root package configuration
|
||||
├── pnpm-workspace.yaml # PNPM workspace configuration
|
||||
└── tsconfig.base.json # Base TypeScript configuration
|
||||
```
|
||||
|
||||
## Applications
|
||||
|
||||
### Client (`/apps/client`)
|
||||
|
||||
The frontend application shared by both server and desktop versions.
|
||||
|
||||
```
|
||||
apps/client/
|
||||
├── src/
|
||||
│ ├── components/ # Core UI components
|
||||
│ ├── entities/ # Frontend entities (FNote, FBranch, etc.)
|
||||
│ ├── services/ # Business logic and API calls
|
||||
│ ├── widgets/ # UI widgets system
|
||||
│ │ ├── type_widgets/ # Note type specific widgets
|
||||
│ │ ├── dialogs/ # Dialog components
|
||||
│ │ └── panels/ # Panel widgets
|
||||
│ ├── public/
|
||||
│ │ ├── fonts/ # Font assets
|
||||
│ │ ├── images/ # Image assets
|
||||
│ │ └── libraries/ # Third-party libraries
|
||||
│ └── desktop.ts # Desktop entry point
|
||||
├── package.json
|
||||
├── project.json # NX project configuration
|
||||
└── vite.config.ts # Vite build configuration
|
||||
```
|
||||
|
||||
#### Key Files
|
||||
|
||||
* `desktop.ts` - Main application initialization
|
||||
* `services/froca.ts` - Frontend cache implementation
|
||||
* `widgets/basic_widget.ts` - Base widget class
|
||||
* `services/server.ts` - API communication layer
|
||||
|
||||
### Server (`/apps/server`)
|
||||
|
||||
The Node.js backend providing API, database, and business logic.
|
||||
|
||||
```
|
||||
apps/server/
|
||||
├── src/
|
||||
│ ├── becca/ # Backend cache system
|
||||
│ │ ├── entities/ # Core entities (BNote, BBranch, etc.)
|
||||
│ │ └── becca.ts # Cache interface
|
||||
│ ├── routes/ # Express routes
|
||||
│ │ ├── api/ # Internal API endpoints
|
||||
│ │ └── pages/ # HTML page routes
|
||||
│ ├── etapi/ # External API
|
||||
│ ├── services/ # Business services
|
||||
│ ├── share/ # Note sharing functionality
|
||||
│ │ └── shaca/ # Share cache
|
||||
│ ├── migrations/ # Database migrations
|
||||
│ ├── assets/
|
||||
│ │ ├── db/ # Database schema
|
||||
│ │ └── doc_notes/ # Documentation notes
|
||||
│ └── main.ts # Server entry point
|
||||
├── package.json
|
||||
├── project.json
|
||||
└── webpack.config.js # Webpack configuration
|
||||
```
|
||||
|
||||
#### Key Services
|
||||
|
||||
* `services/sql.ts` - Database access layer
|
||||
* `services/sync.ts` - Synchronization logic
|
||||
* `services/ws.ts` - WebSocket server
|
||||
* `services/protected_session.ts` - Encryption handling
|
||||
|
||||
### Desktop (`/apps/desktop`)
|
||||
|
||||
Electron wrapper for the desktop application.
|
||||
|
||||
```
|
||||
apps/desktop/
|
||||
├── src/
|
||||
│ ├── main.ts # Electron main process
|
||||
│ ├── preload.ts # Preload script
|
||||
│ ├── services/ # Desktop-specific services
|
||||
│ └── utils/ # Desktop utilities
|
||||
├── resources/ # Desktop resources (icons, etc.)
|
||||
├── package.json
|
||||
└── electron-builder.yml # Electron Builder configuration
|
||||
```
|
||||
|
||||
### Web Clipper (`/apps/web-clipper`)
|
||||
|
||||
Browser extension for saving web content to Trilium.
|
||||
|
||||
```
|
||||
apps/web-clipper/
|
||||
├── src/
|
||||
│ ├── background.js # Background script
|
||||
│ ├── content.js # Content script
|
||||
│ ├── popup/ # Extension popup
|
||||
│ └── options/ # Extension options
|
||||
├── manifest.json # Extension manifest
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
### Commons (`/packages/commons`)
|
||||
|
||||
Shared TypeScript interfaces and utilities used across applications.
|
||||
|
||||
```typescript
|
||||
// packages/commons/src/types.ts
|
||||
export interface NoteRow {
|
||||
noteId: string;
|
||||
title: string;
|
||||
type: string;
|
||||
mime: string;
|
||||
isProtected: boolean;
|
||||
dateCreated: string;
|
||||
dateModified: string;
|
||||
}
|
||||
|
||||
export interface BranchRow {
|
||||
branchId: string;
|
||||
noteId: string;
|
||||
parentNoteId: string;
|
||||
notePosition: number;
|
||||
prefix: string;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### CKEditor5 (`/packages/ckeditor5`)
|
||||
|
||||
Custom CKEditor5 build with Trilium-specific plugins.
|
||||
|
||||
```
|
||||
packages/ckeditor5/
|
||||
├── src/
|
||||
│ ├── ckeditor.ts # Editor configuration
|
||||
│ ├── plugins.ts # Plugin registration
|
||||
│ └── trilium/ # Custom plugins
|
||||
├── theme/ # Editor themes
|
||||
└── package.json
|
||||
```
|
||||
|
||||
#### Custom Plugins
|
||||
|
||||
* **Admonition**: Note boxes with icons
|
||||
* **Footnotes**: Reference footnotes
|
||||
* **Math**: LaTeX equation rendering
|
||||
* **Mermaid**: Diagram integration
|
||||
|
||||
### CodeMirror (`/packages/codemirror`)
|
||||
|
||||
Code editor customizations for the code note type.
|
||||
|
||||
```typescript
|
||||
// packages/codemirror/src/index.ts
|
||||
export function createCodeMirror(element: HTMLElement, options: CodeMirrorOptions) {
|
||||
return CodeMirror(element, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
// Trilium-specific customizations
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
### NX Configuration
|
||||
|
||||
**`nx.json`**
|
||||
|
||||
```json
|
||||
{
|
||||
"tasksRunnerOptions": {
|
||||
"default": {
|
||||
"runner": "nx/tasks-runners/default",
|
||||
"options": {
|
||||
"cacheableOperations": ["build", "test", "lint"],
|
||||
"parallel": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"cache": true
|
||||
},
|
||||
"test": {
|
||||
"cache": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project Configuration
|
||||
|
||||
Each application and package has a `project.json` defining its targets:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "server",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/server",
|
||||
"main": "apps/server/src/main.ts",
|
||||
"tsConfig": "apps/server/tsconfig.app.json"
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/node:node",
|
||||
"options": {
|
||||
"buildTarget": "server:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"options": {
|
||||
"jestConfig": "apps/server/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build Commands
|
||||
|
||||
```sh
|
||||
# Build specific project
|
||||
pnpm nx build server
|
||||
pnpm nx build client
|
||||
|
||||
# Build all projects
|
||||
pnpm nx run-many --target=build --all
|
||||
|
||||
# Build with dependencies
|
||||
pnpm nx build server --with-deps
|
||||
|
||||
# Production build
|
||||
pnpm nx build server --configuration=production
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```sh
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Enable corepack for pnpm
|
||||
corepack enable
|
||||
|
||||
# Build all packages
|
||||
pnpm nx run-many --target=build --all
|
||||
```
|
||||
|
||||
### Development Commands
|
||||
|
||||
```sh
|
||||
# Start development server
|
||||
pnpm run server:start
|
||||
# or
|
||||
pnpm nx run server:serve
|
||||
|
||||
# Start desktop app
|
||||
pnpm nx run desktop:serve
|
||||
|
||||
# Run client dev server
|
||||
pnpm nx run client:serve
|
||||
|
||||
# Watch mode for packages
|
||||
pnpm nx run commons:build --watch
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```sh
|
||||
# Run all tests
|
||||
pnpm test:all
|
||||
|
||||
# Run tests for specific project
|
||||
pnpm nx test server
|
||||
pnpm nx test client
|
||||
|
||||
# Run tests in watch mode
|
||||
pnpm nx test server --watch
|
||||
|
||||
# Generate coverage
|
||||
pnpm nx test server --coverage
|
||||
```
|
||||
|
||||
### Linting and Type Checking
|
||||
|
||||
```sh
|
||||
# Lint specific project
|
||||
pnpm nx lint server
|
||||
|
||||
# Type check
|
||||
pnpm nx run server:typecheck
|
||||
|
||||
# Lint all projects
|
||||
pnpm nx run-many --target=lint --all
|
||||
|
||||
# Fix lint issues
|
||||
pnpm nx lint server --fix
|
||||
```
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### Package Dependencies
|
||||
|
||||
Dependencies are managed at both root and project levels:
|
||||
|
||||
```json
|
||||
// Root package.json - shared dev dependencies
|
||||
{
|
||||
"devDependencies": {
|
||||
"@nx/workspace": "^17.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
// Project package.json - project-specific dependencies
|
||||
{
|
||||
"dependencies": {
|
||||
"express": "^4.18.0",
|
||||
"better-sqlite3": "^9.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Dependencies
|
||||
|
||||
```sh
|
||||
# Add to root
|
||||
pnpm add -D typescript
|
||||
|
||||
# Add to specific project
|
||||
pnpm add express --filter server
|
||||
|
||||
# Add to multiple projects
|
||||
pnpm add lodash --filter server --filter client
|
||||
```
|
||||
|
||||
### Workspace References
|
||||
|
||||
Internal packages are referenced using workspace protocol:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@triliumnext/commons": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
### Base Configuration
|
||||
|
||||
**`tsconfig.base.json`**
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@triliumnext/commons": ["packages/commons/src/index.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project-Specific Configuration
|
||||
|
||||
```json
|
||||
// apps/server/tsconfig.json
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["**/*.spec.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
## Build Optimization
|
||||
|
||||
### NX Cloud
|
||||
|
||||
```sh
|
||||
# Enable NX Cloud for distributed caching
|
||||
pnpm nx connect-to-nx-cloud
|
||||
```
|
||||
|
||||
### Affected Commands
|
||||
|
||||
```sh
|
||||
# Build only affected projects
|
||||
pnpm nx affected:build --base=main
|
||||
|
||||
# Test only affected projects
|
||||
pnpm nx affected:test --base=main
|
||||
|
||||
# Lint only affected projects
|
||||
pnpm nx affected:lint --base=main
|
||||
```
|
||||
|
||||
### Build Caching
|
||||
|
||||
NX caches build outputs to speed up subsequent builds:
|
||||
|
||||
```sh
|
||||
# Clear cache
|
||||
pnpm nx reset
|
||||
|
||||
# Run with cache disabled
|
||||
pnpm nx build server --skip-nx-cache
|
||||
|
||||
# See cache statistics
|
||||
pnpm nx report
|
||||
```
|
||||
|
||||
## Production Builds
|
||||
|
||||
### Building for Production
|
||||
|
||||
```sh
|
||||
# Build server for production
|
||||
pnpm nx build server --configuration=production
|
||||
|
||||
# Build client with optimization
|
||||
pnpm nx build client --configuration=production
|
||||
|
||||
# Build desktop app
|
||||
pnpm nx build desktop --configuration=production
|
||||
pnpm electron:build # Creates distributables
|
||||
```
|
||||
|
||||
### Docker Build
|
||||
|
||||
```dockerfile
|
||||
# Multi-stage build
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json pnpm-lock.yaml ./
|
||||
RUN corepack enable && pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm nx build server --configuration=production
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist/apps/server ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
CMD ["node", "main.js"]
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- run: pnpm nx affected:lint --base=origin/main
|
||||
|
||||
- run: pnpm nx affected:test --base=origin/main
|
||||
|
||||
- run: pnpm nx affected:build --base=origin/main
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Build Cache Issues**
|
||||
|
||||
```sh
|
||||
# Clear NX cache
|
||||
pnpm nx reset
|
||||
|
||||
# Clear node_modules and reinstall
|
||||
rm -rf node_modules
|
||||
pnpm install
|
||||
```
|
||||
2. **Dependency Version Conflicts**
|
||||
|
||||
```sh
|
||||
# Check for duplicate packages
|
||||
pnpm list --depth=0
|
||||
|
||||
# Update all dependencies
|
||||
pnpm update --recursive
|
||||
```
|
||||
3. **TypeScript Path Resolution**
|
||||
|
||||
```sh
|
||||
# Verify TypeScript paths
|
||||
pnpm nx run server:typecheck --traceResolution
|
||||
```
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```sh
|
||||
# Show project graph
|
||||
pnpm nx graph
|
||||
|
||||
# Show project dependencies
|
||||
pnpm nx print-affected --type=app --select=projects
|
||||
|
||||
# Verbose output
|
||||
pnpm nx build server --verbose
|
||||
|
||||
# Profile build performance
|
||||
pnpm nx build server --profile
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Project Structure
|
||||
|
||||
1. **Keep packages focused**: Each package should have a single, clear purpose
|
||||
2. **Minimize circular dependencies**: Use dependency graph to identify issues
|
||||
3. **Share common code**: Extract shared logic to packages/commons
|
||||
|
||||
### Development
|
||||
|
||||
1. **Use NX generators**: Generate consistent code structure
|
||||
2. **Leverage caching**: Don't skip-nx-cache unless debugging
|
||||
3. **Run affected commands**: Save time by only building/testing changed code
|
||||
|
||||
### Testing
|
||||
|
||||
1. **Colocate tests**: Keep test files next to source files
|
||||
2. **Use workspace scripts**: Define common scripts in root package.json
|
||||
3. **Parallel execution**: Use `--parallel` flag for faster execution
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Environment Setup](#root/tFVKyUp99QEc) - Development environment setup
|
||||
* [Project Structure](#root/NurKhC1Zq1CN) - Detailed project structure
|
||||
* [Build Information](#root/m5GSI5gIyqs5) - Build details
|
||||
97
docs/Developer Guide/Developer Guide/Architecture/README.md
vendored
Normal file
97
docs/Developer Guide/Developer Guide/Architecture/README.md
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
# README
|
||||
## Trilium Architecture Documentation
|
||||
|
||||
This comprehensive guide documents the architecture of Trilium Notes, providing developers with detailed information about the system's core components, data flow, and design patterns.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Three-Layer Cache System](Three-Layer-Cache-System.md)
|
||||
2. [Entity System](Entity-System.md)
|
||||
3. [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md)
|
||||
4. [API Architecture](API-Architecture.md)
|
||||
5. [Monorepo Structure](Monorepo-Structure.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Trilium Notes is built as a TypeScript monorepo using NX, featuring a sophisticated architecture that balances performance, flexibility, and maintainability. The system is designed around several key architectural patterns:
|
||||
|
||||
* **Three-layer caching system** for optimal performance across backend, frontend, and shared content
|
||||
* **Entity-based data model** supporting hierarchical note structures with multiple parent relationships
|
||||
* **Widget-based UI architecture** enabling modular and extensible interface components
|
||||
* **Multiple API layers** for internal operations, external integrations, and real-time synchronization
|
||||
* **Monorepo structure** facilitating code sharing and consistent development patterns
|
||||
|
||||
## Quick Start for Developers
|
||||
|
||||
If you're new to Trilium development, start with these sections:
|
||||
|
||||
1. [Monorepo Structure](Monorepo-Structure.md) - Understand the project organization
|
||||
2. [Entity System](Entity-System.md) - Learn about the core data model
|
||||
3. [Three-Layer Cache System](Three-Layer-Cache-System.md) - Understand data flow and caching
|
||||
|
||||
For UI development, refer to:
|
||||
|
||||
* [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md)
|
||||
|
||||
For API integration, see:
|
||||
|
||||
* [API Architecture](API-Architecture.md)
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
### Performance First
|
||||
|
||||
* Lazy loading of note content
|
||||
* Efficient caching at multiple layers
|
||||
* Optimized database queries with prepared statements
|
||||
|
||||
### Flexibility
|
||||
|
||||
* Support for multiple note types
|
||||
* Extensible through scripting
|
||||
* Plugin architecture for UI widgets
|
||||
|
||||
### Data Integrity
|
||||
|
||||
* Transactional database operations
|
||||
* Revision history for all changes
|
||||
* Synchronization conflict resolution
|
||||
|
||||
### Security
|
||||
|
||||
* Per-note encryption
|
||||
* Protected sessions
|
||||
* API authentication tokens
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Setup Development Environment**
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm run server:start
|
||||
```
|
||||
2. **Make Changes**
|
||||
|
||||
* Backend changes in `apps/server/src/`
|
||||
* Frontend changes in `apps/client/src/`
|
||||
* Shared code in `packages/`
|
||||
3. **Test Your Changes**
|
||||
|
||||
```sh
|
||||
pnpm test:all
|
||||
pnpm nx run <project>:lint
|
||||
```
|
||||
4. **Build for Production**
|
||||
|
||||
```sh
|
||||
pnpm nx build server
|
||||
pnpm nx build client
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
|
||||
* [Development Environment Setup](#root/tFVKyUp99QEc)
|
||||
* [Adding a New Note Type](#root/6aV1LKciq0CF)
|
||||
* [Database Schema](#root/eZcnGfMUmic0)
|
||||
* [Script API Documentation](#root/7Pp4moCrBVzA)
|
||||
374
docs/Developer Guide/Developer Guide/Architecture/Three-Layer-Cache-System.md
vendored
Normal file
374
docs/Developer Guide/Developer Guide/Architecture/Three-Layer-Cache-System.md
vendored
Normal file
@@ -0,0 +1,374 @@
|
||||
# Three-Layer-Cache-System
|
||||
## Three-Layer Cache System Architecture
|
||||
|
||||
Trilium implements a sophisticated three-layer caching system to optimize performance and reduce database load. This architecture ensures fast access to frequently used data while maintaining consistency across different application contexts.
|
||||
|
||||
## Overview
|
||||
|
||||
The three cache layers are:
|
||||
|
||||
1. **Becca** (Backend Cache) - Server-side entity cache
|
||||
2. **Froca** (Frontend Cache) - Client-side mirror of backend data
|
||||
3. **Shaca** (Share Cache) - Optimized cache for shared/published notes
|
||||
|
||||
```
|
||||
graph TB
|
||||
subgraph "Database Layer"
|
||||
DB[(SQLite Database)]
|
||||
end
|
||||
|
||||
subgraph "Backend Layer"
|
||||
Becca[Becca Cache<br/>Backend Cache]
|
||||
API[API Layer]
|
||||
end
|
||||
|
||||
subgraph "Frontend Layer"
|
||||
Froca[Froca Cache<br/>Frontend Cache]
|
||||
UI[UI Components]
|
||||
end
|
||||
|
||||
subgraph "Share Layer"
|
||||
Shaca[Shaca Cache<br/>Share Cache]
|
||||
Share[Public Share Interface]
|
||||
end
|
||||
|
||||
DB <--> Becca
|
||||
Becca <--> API
|
||||
API <--> Froca
|
||||
Froca <--> UI
|
||||
DB <--> Shaca
|
||||
Shaca <--> Share
|
||||
|
||||
style Becca fill:#e1f5fe
|
||||
style Froca fill:#fff3e0
|
||||
style Shaca fill:#f3e5f5
|
||||
```
|
||||
|
||||
## Becca (Backend Cache)
|
||||
|
||||
**Location**: `/apps/server/src/becca/`
|
||||
|
||||
Becca is the authoritative cache layer that maintains all notes, branches, attributes, and options in server memory.
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Becca Interface (`becca-interface.ts`)
|
||||
|
||||
```typescript
|
||||
export default class Becca {
|
||||
loaded: boolean;
|
||||
notes: Record<string, BNote>;
|
||||
branches: Record<string, BBranch>;
|
||||
childParentToBranch: Record<string, BBranch>;
|
||||
attributes: Record<string, BAttribute>;
|
||||
attributeIndex: Record<string, BAttribute[]>;
|
||||
options: Record<string, BOption>;
|
||||
etapiTokens: Record<string, BEtapiToken>;
|
||||
allNoteSetCache: NoteSet | null;
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
* **In-memory storage**: All active entities are kept in memory for fast access
|
||||
* **Lazy loading**: Related entities (revisions, attachments) loaded on demand
|
||||
* **Index structures**: Optimized lookups via `childParentToBranch` and `attributeIndex`
|
||||
* **Cache invalidation**: Automatic cache updates on entity changes
|
||||
* **Protected note decryption**: On-demand decryption of encrypted content
|
||||
|
||||
### Usage Example
|
||||
|
||||
```typescript
|
||||
import becca from "./becca/becca.js";
|
||||
|
||||
// Get a note
|
||||
const note = becca.getNote("noteId");
|
||||
|
||||
// Find attributes by type and name
|
||||
const labels = becca.findAttributes("label", "todoItem");
|
||||
|
||||
// Get branch relationships
|
||||
const branch = becca.getBranchFromChildAndParent(childId, parentId);
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Initialization**: Load all notes, branches, and attributes from database
|
||||
2. **Access**: Direct memory access for cached entities
|
||||
3. **Updates**: Write-through cache with immediate database persistence
|
||||
4. **Invalidation**: Automatic cache refresh on entity changes
|
||||
|
||||
## Froca (Frontend Cache)
|
||||
|
||||
**Location**: `/apps/client/src/services/froca.ts`
|
||||
|
||||
Froca is the frontend mirror of Becca, maintaining a subset of backend data for client-side operations.
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Froca Implementation (`froca.ts`)
|
||||
|
||||
```typescript
|
||||
class FrocaImpl implements Froca {
|
||||
notes: Record<string, FNote>;
|
||||
branches: Record<string, FBranch>;
|
||||
attributes: Record<string, FAttribute>;
|
||||
attachments: Record<string, FAttachment>;
|
||||
blobPromises: Record<string, Promise<FBlob | null> | null>;
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
* **Lazy loading**: Notes loaded on-demand with their immediate context
|
||||
* **Subtree loading**: Efficient loading of note hierarchies
|
||||
* **Real-time updates**: WebSocket synchronization with backend changes
|
||||
* **Search note support**: Virtual branches for search results
|
||||
* **Promise-based blob loading**: Asynchronous content loading
|
||||
|
||||
### Loading Strategy
|
||||
|
||||
```typescript
|
||||
// Initial load - loads root and immediate children
|
||||
await froca.loadInitialTree();
|
||||
|
||||
// Load subtree on demand
|
||||
const note = await froca.loadSubTree(noteId);
|
||||
|
||||
// Reload specific notes
|
||||
await froca.reloadNotes([noteId1, noteId2]);
|
||||
```
|
||||
|
||||
### Synchronization
|
||||
|
||||
Froca maintains consistency with Becca through:
|
||||
|
||||
1. **Initial sync**: Load essential tree structure on startup
|
||||
2. **On-demand loading**: Fetch notes as needed
|
||||
3. **WebSocket updates**: Real-time push of changes from backend
|
||||
4. **Batch reloading**: Efficient refresh of multiple notes
|
||||
|
||||
## Shaca (Share Cache)
|
||||
|
||||
**Location**: `/apps/server/src/share/shaca/`
|
||||
|
||||
Shaca is a specialized cache for publicly shared notes, optimized for read-only access.
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Shaca Interface (`shaca-interface.ts`)
|
||||
|
||||
```typescript
|
||||
export default class Shaca {
|
||||
notes: Record<string, SNote>;
|
||||
branches: Record<string, SBranch>;
|
||||
childParentToBranch: Record<string, SBranch>;
|
||||
attributes: Record<string, SAttribute>;
|
||||
attachments: Record<string, SAttachment>;
|
||||
aliasToNote: Record<string, SNote>;
|
||||
shareRootNote: SNote | null;
|
||||
shareIndexEnabled: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
* **Read-only optimization**: Streamlined for public access
|
||||
* **Alias support**: URL-friendly note access via aliases
|
||||
* **Share index**: Optional indexing of all shared subtrees
|
||||
* **Minimal memory footprint**: Only shared content cached
|
||||
* **Security isolation**: Separate from main application cache
|
||||
|
||||
### Usage Patterns
|
||||
|
||||
```typescript
|
||||
// Get shared note by ID
|
||||
const note = shaca.getNote(noteId);
|
||||
|
||||
// Access via alias
|
||||
const aliasedNote = shaca.aliasToNote[alias];
|
||||
|
||||
// Check if note is shared
|
||||
const isShared = shaca.hasNote(noteId);
|
||||
```
|
||||
|
||||
## Cache Interaction and Data Flow
|
||||
|
||||
### 1\. Create/Update Flow
|
||||
|
||||
```
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Froca
|
||||
participant API
|
||||
participant Becca
|
||||
participant DB
|
||||
|
||||
Client->>API: Update Note
|
||||
API->>Becca: Update Cache
|
||||
Becca->>DB: Persist Change
|
||||
Becca->>API: Confirm
|
||||
API->>Froca: Push Update (WebSocket)
|
||||
Froca->>Client: Update UI
|
||||
```
|
||||
|
||||
### 2\. Read Flow
|
||||
|
||||
```
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Froca
|
||||
participant API
|
||||
participant Becca
|
||||
|
||||
Client->>Froca: Request Note
|
||||
alt Note in Cache
|
||||
Froca->>Client: Return Cached Note
|
||||
else Note not in Cache
|
||||
Froca->>API: Fetch Note
|
||||
API->>Becca: Get Note
|
||||
Becca->>API: Return Note
|
||||
API->>Froca: Send Note Data
|
||||
Froca->>Froca: Cache Note
|
||||
Froca->>Client: Return Note
|
||||
end
|
||||
```
|
||||
|
||||
### 3\. Share Access Flow
|
||||
|
||||
```
|
||||
sequenceDiagram
|
||||
participant Browser
|
||||
participant ShareUI
|
||||
participant Shaca
|
||||
participant DB
|
||||
|
||||
Browser->>ShareUI: Access Shared URL
|
||||
ShareUI->>Shaca: Get Shared Note
|
||||
alt Note in Cache
|
||||
Shaca->>ShareUI: Return Cached
|
||||
else Not in Cache
|
||||
Shaca->>DB: Load Shared Tree
|
||||
DB->>Shaca: Return Data
|
||||
Shaca->>Shaca: Build Cache
|
||||
Shaca->>ShareUI: Return Note
|
||||
end
|
||||
ShareUI->>Browser: Render Content
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Management
|
||||
|
||||
* **Becca**: Keeps entire note tree in memory (~100-500MB for typical use)
|
||||
* **Froca**: Loads notes on-demand, automatic cleanup of unused notes
|
||||
* **Shaca**: Minimal footprint, only shared content
|
||||
|
||||
### Cache Warming
|
||||
|
||||
* **Becca**: Full load on server startup
|
||||
* **Froca**: Progressive loading based on user navigation
|
||||
* **Shaca**: Lazy loading with configurable index
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
1. **Attribute Indexing**: Pre-built indexes for fast attribute queries
|
||||
2. **Batch Operations**: Group updates to minimize round trips
|
||||
3. **Partial Loading**: Load only required fields for lists
|
||||
4. **WebSocket Compression**: Compressed real-time updates
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use Each Cache
|
||||
|
||||
**Use Becca when**:
|
||||
|
||||
* Implementing server-side business logic
|
||||
* Performing bulk operations
|
||||
* Handling synchronization
|
||||
* Managing protected notes
|
||||
|
||||
**Use Froca when**:
|
||||
|
||||
* Building UI components
|
||||
* Handling user interactions
|
||||
* Displaying note content
|
||||
* Managing client state
|
||||
|
||||
**Use Shaca when**:
|
||||
|
||||
* Serving public content
|
||||
* Building share pages
|
||||
* Implementing read-only access
|
||||
* Creating public APIs
|
||||
|
||||
### Cache Invalidation
|
||||
|
||||
```typescript
|
||||
// Becca - automatic on entity save
|
||||
note.save(); // Cache updated automatically
|
||||
|
||||
// Froca - manual reload when needed
|
||||
await froca.reloadNotes([noteId]);
|
||||
|
||||
// Shaca - rebuild on share changes
|
||||
shaca.reset();
|
||||
shaca.load();
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
// Becca - throw on missing required entities
|
||||
const note = becca.getNoteOrThrow(noteId); // throws NotFoundError
|
||||
|
||||
// Froca - graceful degradation
|
||||
const note = await froca.getNote(noteId);
|
||||
if (!note) {
|
||||
// Handle missing note
|
||||
}
|
||||
|
||||
// Shaca - check existence first
|
||||
if (shaca.hasNote(noteId)) {
|
||||
const note = shaca.getNote(noteId);
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Cache Inconsistency**
|
||||
|
||||
* Symptom: UI shows outdated data
|
||||
* Solution: Force reload with `froca.reloadNotes()`
|
||||
2. **Memory Growth**
|
||||
|
||||
* Symptom: Server memory usage increases
|
||||
* Solution: Check for memory leaks in custom scripts
|
||||
3. **Slow Initial Load**
|
||||
|
||||
* Symptom: Long startup time
|
||||
* Solution: Optimize database queries, add indexes
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```javascript
|
||||
// Check cache sizes
|
||||
console.log('Becca notes:', Object.keys(becca.notes).length);
|
||||
console.log('Froca notes:', Object.keys(froca.notes).length);
|
||||
console.log('Shaca notes:', Object.keys(shaca.notes).length);
|
||||
|
||||
// Force cache refresh
|
||||
await froca.loadInitialTree();
|
||||
|
||||
// Clear and reload Shaca
|
||||
shaca.reset();
|
||||
await shaca.load();
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Entity System](Entity-System.md) - Detailed entity documentation
|
||||
* [Database Schema](#root/eZcnGfMUmic0) - Database structure
|
||||
* [WebSocket Synchronization](#root/UBgXVlHv3d66) - Real-time updates
|
||||
514
docs/Developer Guide/Developer Guide/Architecture/Widget-Based-UI-Architecture.md
vendored
Normal file
514
docs/Developer Guide/Developer Guide/Architecture/Widget-Based-UI-Architecture.md
vendored
Normal file
@@ -0,0 +1,514 @@
|
||||
# Widget-Based-UI-Architecture
|
||||
## Widget-Based UI Architecture
|
||||
|
||||
Trilium's user interface is built on a widget system where each UI element is a self-contained component. This approach makes the interface modular, allowing developers to create custom widgets and extend the application's functionality.
|
||||
|
||||
## Understanding Widgets
|
||||
|
||||
Widgets in Trilium form a hierarchy, with each level adding specific capabilities. At the base, every widget is a Component that can have children and communicate with other widgets. The BasicWidget layer adds DOM manipulation, while NoteContextAwareWidget provides automatic updates when notes change.
|
||||
|
||||
## Core Widget Classes
|
||||
|
||||
### Component (Base Class)
|
||||
|
||||
**Location**: `/apps/client/src/components/component.js`
|
||||
|
||||
Every UI element in Trilium inherits from Component, which provides the foundation for parent-child relationships and event communication between widgets.
|
||||
|
||||
### BasicWidget
|
||||
|
||||
**Location**: `/apps/client/src/widgets/basic_widget.ts`
|
||||
|
||||
BasicWidget extends Component with DOM manipulation capabilities. It provides a fluent API for styling and composing UI elements:
|
||||
|
||||
```typescript
|
||||
class MyWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="my-widget">');
|
||||
this.$widget.append($('<h3>').text('Title'));
|
||||
return this.$widget;
|
||||
}
|
||||
}
|
||||
|
||||
// Compose widgets using method chaining
|
||||
const container = new FlexContainer('column')
|
||||
.id('main-container')
|
||||
.css('padding', '10px')
|
||||
.child(new MyWidget());
|
||||
```
|
||||
|
||||
The chaining API makes it easy to build complex UIs declaratively. Methods like `css()`, `class()`, and `child()` return the widget itself, allowing you to chain multiple operations together.
|
||||
|
||||
### NoteContextAwareWidget
|
||||
|
||||
**Location**: `/apps/client/src/widgets/note_context_aware_widget.ts`
|
||||
|
||||
Widgets that need to react to note changes extend NoteContextAwareWidget. This class automatically calls your widget's methods when the active note changes, making it easy to keep the UI synchronized with the current note.
|
||||
|
||||
```typescript
|
||||
class MyNoteWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note) {
|
||||
// Automatically called when note changes
|
||||
this.$widget.find('.title').text(note.title);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key lifecycle methods:
|
||||
|
||||
* `refreshWithNote(note)` - Called when the active note changes
|
||||
* `noteSwitched()` - Called after switching to a different note
|
||||
* `noteTypeMimeChanged()` - Called when the note's type changes
|
||||
|
||||
The widget automatically maintains references to the current note, making it simple to build note-aware UI components.
|
||||
|
||||
### RightPanelWidget
|
||||
|
||||
**Location**: `/apps/client/src/widgets/right_panel_widget.ts`
|
||||
|
||||
Widgets in the right sidebar extend RightPanelWidget. These widgets appear as collapsible panels and can show note-specific information or tools.
|
||||
|
||||
```typescript
|
||||
class InfoWidget extends RightPanelWidget {
|
||||
getTitle() { return "Note Info"; }
|
||||
getIcon() { return "info"; }
|
||||
|
||||
async doRenderBody() {
|
||||
return $('<div class="info-widget">');
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$body.find('.created').text(`Created: ${note.dateCreated}`);
|
||||
this.$body.find('.modified').text(`Modified: ${note.dateModified}`);
|
||||
|
||||
const wordCount = this.calculateWordCount(await note.getContent());
|
||||
this.$body.find('.word-count').text(`Words: ${wordCount}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Type-Specific Widgets
|
||||
|
||||
**Location**: `/apps/client/src/widgets/type_widgets/`
|
||||
|
||||
Each note type has a specialized widget for rendering and editing.
|
||||
|
||||
### TypeWidget Interface
|
||||
|
||||
```typescript
|
||||
abstract class TypeWidget extends NoteContextAwareWidget {
|
||||
abstract static getType(): string;
|
||||
|
||||
// Content management
|
||||
async getContent(): Promise<string>;
|
||||
async saveContent(content: string): Promise<void>;
|
||||
|
||||
// Focus management
|
||||
async focus(): Promise<void>;
|
||||
async blur(): Promise<void>;
|
||||
|
||||
// Cleanup
|
||||
async cleanup(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### Common Type Widgets
|
||||
|
||||
#### TextTypeWidget
|
||||
|
||||
```typescript
|
||||
class TextTypeWidget extends TypeWidget {
|
||||
static getType() { return 'text'; }
|
||||
|
||||
private textEditor: TextEditor;
|
||||
|
||||
async doRender() {
|
||||
const $editor = $('<div class="ck-editor">');
|
||||
this.textEditor = await TextEditor.create($editor[0], {
|
||||
noteId: this.noteId,
|
||||
content: await this.note.getContent()
|
||||
});
|
||||
|
||||
return $editor;
|
||||
}
|
||||
|
||||
async getContent() {
|
||||
return this.textEditor.getData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### CodeTypeWidget
|
||||
|
||||
```typescript
|
||||
class CodeTypeWidget extends TypeWidget {
|
||||
static getType() { return 'code'; }
|
||||
|
||||
private codeMirror: CodeMirror;
|
||||
|
||||
async doRender() {
|
||||
const $container = $('<div class="code-editor">');
|
||||
|
||||
this.codeMirror = CodeMirror($container[0], {
|
||||
value: await this.note.getContent(),
|
||||
mode: this.note.mime,
|
||||
theme: 'default',
|
||||
lineNumbers: true
|
||||
});
|
||||
|
||||
return $container;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Composition
|
||||
|
||||
### Container Widgets
|
||||
|
||||
```typescript
|
||||
// Flexible container layouts
|
||||
class FlexContainer extends BasicWidget {
|
||||
constructor(private direction: 'row' | 'column') {
|
||||
super();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $('<div class="flex-container">')
|
||||
.css('display', 'flex')
|
||||
.css('flex-direction', this.direction);
|
||||
|
||||
for (const child of this.children) {
|
||||
this.$widget.append(child.render());
|
||||
}
|
||||
|
||||
return this.$widget;
|
||||
}
|
||||
}
|
||||
|
||||
// Tab container
|
||||
class TabContainer extends BasicWidget {
|
||||
private tabs: Array<{title: string, widget: BasicWidget}> = [];
|
||||
|
||||
addTab(title: string, widget: BasicWidget) {
|
||||
this.tabs.push({title, widget});
|
||||
this.child(widget);
|
||||
return this;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
// Render tab headers and content panels
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Composite Widgets
|
||||
|
||||
```typescript
|
||||
class NoteEditorWidget extends NoteContextAwareWidget {
|
||||
private typeWidget: TypeWidget;
|
||||
private titleWidget: NoteTitleWidget;
|
||||
private toolbarWidget: NoteToolbarWidget;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.child(
|
||||
this.toolbarWidget = new NoteToolbarWidget(),
|
||||
this.titleWidget = new NoteTitleWidget(),
|
||||
// Type widget added dynamically
|
||||
);
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
// Remove old type widget
|
||||
if (this.typeWidget) {
|
||||
this.typeWidget.remove();
|
||||
}
|
||||
|
||||
// Add appropriate type widget
|
||||
const WidgetClass = typeWidgetService.getWidgetClass(note.type);
|
||||
this.typeWidget = new WidgetClass();
|
||||
this.child(this.typeWidget);
|
||||
|
||||
await this.typeWidget.refresh();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Communication
|
||||
|
||||
### Event System
|
||||
|
||||
```typescript
|
||||
// Publishing events
|
||||
class PublisherWidget extends BasicWidget {
|
||||
async handleClick() {
|
||||
// Local event
|
||||
this.trigger('itemSelected', { itemId: '123' });
|
||||
|
||||
// Global event
|
||||
appContext.triggerEvent('noteChanged', { noteId: this.noteId });
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribing to events
|
||||
class SubscriberWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Local event subscription
|
||||
this.on('itemSelected', (event) => {
|
||||
console.log('Item selected:', event.itemId);
|
||||
});
|
||||
|
||||
// Global event subscription
|
||||
appContext.addEventListener('noteChanged', (event) => {
|
||||
this.handleNoteChange(event.noteId);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command System
|
||||
|
||||
```typescript
|
||||
// Registering commands
|
||||
class CommandWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.bindCommand('saveNote', () => this.saveNote());
|
||||
this.bindCommand('deleteNote', () => this.deleteNote());
|
||||
}
|
||||
|
||||
getCommands() {
|
||||
return [
|
||||
{
|
||||
command: 'myWidget:doAction',
|
||||
handler: () => this.doAction(),
|
||||
hotkey: 'ctrl+shift+a'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Widget Development
|
||||
|
||||
### Creating Custom Widgets
|
||||
|
||||
```typescript
|
||||
// 1. Define widget class
|
||||
class TaskListWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="task-list-widget">');
|
||||
this.$list = $('<ul>').appendTo(this.$widget);
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
const tasks = await this.loadTasks(note);
|
||||
|
||||
this.$list.empty();
|
||||
for (const task of tasks) {
|
||||
$('<li>')
|
||||
.text(task.title)
|
||||
.toggleClass('completed', task.completed)
|
||||
.appendTo(this.$list);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadTasks(note: FNote) {
|
||||
// Load task data from note attributes
|
||||
const taskLabels = note.getLabels('task');
|
||||
return taskLabels.map(label => JSON.parse(label.value));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Register widget
|
||||
api.addWidget(TaskListWidget);
|
||||
```
|
||||
|
||||
### Widget Lifecycle
|
||||
|
||||
```typescript
|
||||
class LifecycleWidget extends NoteContextAwareWidget {
|
||||
// 1. Construction
|
||||
constructor() {
|
||||
super();
|
||||
console.log('Widget constructed');
|
||||
}
|
||||
|
||||
// 2. Initial render
|
||||
doRender() {
|
||||
console.log('Initial render');
|
||||
return $('<div>');
|
||||
}
|
||||
|
||||
// 3. Context initialization
|
||||
async refresh() {
|
||||
console.log('Context refresh');
|
||||
await super.refresh();
|
||||
}
|
||||
|
||||
// 4. Note updates
|
||||
async refreshWithNote(note: FNote) {
|
||||
console.log('Note refresh:', note.noteId);
|
||||
}
|
||||
|
||||
// 5. Cleanup
|
||||
async cleanup() {
|
||||
console.log('Widget cleanup');
|
||||
// Release resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
class LazyWidget extends BasicWidget {
|
||||
private contentLoaded = false;
|
||||
|
||||
async becomeVisible() {
|
||||
if (!this.contentLoaded) {
|
||||
await this.loadContent();
|
||||
this.contentLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadContent() {
|
||||
// Heavy content loading
|
||||
const data = await server.get('expensive-data');
|
||||
this.renderContent(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debouncing Updates
|
||||
|
||||
```typescript
|
||||
class DebouncedWidget extends NoteContextAwareWidget {
|
||||
private refreshDebounced = utils.debounce(
|
||||
() => this.doRefresh(),
|
||||
500
|
||||
);
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
// Debounce rapid updates
|
||||
this.refreshDebounced();
|
||||
}
|
||||
|
||||
private async doRefresh() {
|
||||
// Actual refresh logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Virtual Scrolling
|
||||
|
||||
```typescript
|
||||
class VirtualListWidget extends BasicWidget {
|
||||
private visibleItems: any[] = [];
|
||||
|
||||
renderVisibleItems(scrollTop: number) {
|
||||
const itemHeight = 30;
|
||||
const containerHeight = this.$widget.height();
|
||||
|
||||
const startIndex = Math.floor(scrollTop / itemHeight);
|
||||
const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
|
||||
|
||||
this.visibleItems = this.allItems.slice(startIndex, endIndex);
|
||||
this.renderItems();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Widget Design
|
||||
|
||||
1. **Single Responsibility**: Each widget should have one clear purpose
|
||||
2. **Composition over Inheritance**: Use composition for complex UIs
|
||||
3. **Lazy Initialization**: Load resources only when needed
|
||||
4. **Event Cleanup**: Remove event listeners in cleanup()
|
||||
|
||||
### State Management
|
||||
|
||||
```typescript
|
||||
class StatefulWidget extends NoteContextAwareWidget {
|
||||
private state = {
|
||||
isExpanded: false,
|
||||
selectedItems: new Set<string>()
|
||||
};
|
||||
|
||||
setState(updates: Partial<typeof this.state>) {
|
||||
Object.assign(this.state, updates);
|
||||
this.renderState();
|
||||
}
|
||||
|
||||
private renderState() {
|
||||
this.$widget.toggleClass('expanded', this.state.isExpanded);
|
||||
// Update DOM based on state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
class ResilientWidget extends BasicWidget {
|
||||
async refreshWithNote(note: FNote) {
|
||||
try {
|
||||
await this.loadData(note);
|
||||
} catch (error) {
|
||||
this.showError('Failed to load data');
|
||||
console.error('Widget error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
this.$widget.html(`
|
||||
<div class="alert alert-danger">
|
||||
${message}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Widgets
|
||||
|
||||
```typescript
|
||||
// Widget test example
|
||||
describe('TaskListWidget', () => {
|
||||
let widget: TaskListWidget;
|
||||
let note: FNote;
|
||||
|
||||
beforeEach(() => {
|
||||
widget = new TaskListWidget();
|
||||
note = createMockNote({
|
||||
noteId: 'test123',
|
||||
attributes: [
|
||||
{ type: 'label', name: 'task', value: '{"title":"Task 1"}' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should render tasks', async () => {
|
||||
await widget.refreshWithNote(note);
|
||||
|
||||
const tasks = widget.$widget.find('li');
|
||||
expect(tasks.length).toBe(1);
|
||||
expect(tasks.text()).toBe('Task 1');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Frontend Basics](#root/8Cc2LnZFcF3K) - Frontend scripting guide
|
||||
* [Custom Widgets](#root/VAerafq2qHO1) - Creating custom widgets
|
||||
* [Script API](#root/7Pp4moCrBVzA) - Widget API reference
|
||||
1422
docs/Developer Guide/Developer Guide/Plugin Development/Backend Script Development.md
vendored
Normal file
1422
docs/Developer Guide/Developer Guide/Plugin Development/Backend Script Development.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1624
docs/Developer Guide/Developer Guide/Plugin Development/Custom Note Type Development.md
vendored
Normal file
1624
docs/Developer Guide/Developer Guide/Plugin Development/Custom Note Type Development.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
694
docs/Developer Guide/Developer Guide/Plugin Development/Custom Widget Development Guid.md
vendored
Normal file
694
docs/Developer Guide/Developer Guide/Plugin Development/Custom Widget Development Guid.md
vendored
Normal file
@@ -0,0 +1,694 @@
|
||||
# Custom Widget Development Guide
|
||||
Widgets are the building blocks of Trilium's user interface. This guide shows you how to create your own widgets to extend Trilium with custom functionality.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To develop widgets, you'll need basic JavaScript knowledge and familiarity with jQuery. Widgets in Trilium follow a simple hierarchy where each type adds specific capabilities - BasicWidget for general UI, NoteContextAwareWidget for note-responsive widgets, and RightPanelWidget for sidebar panels.
|
||||
|
||||
## Creating Your First Widget
|
||||
|
||||
### Basic Widget
|
||||
|
||||
Start with a simple widget that displays static content:
|
||||
|
||||
```javascript
|
||||
class MyWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>Hello from my widget!</div>');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Note-Aware Widget
|
||||
|
||||
To make your widget respond to note changes, extend NoteContextAwareWidget:
|
||||
|
||||
```javascript
|
||||
class NoteInfoWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="note-info"></div>');
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.$widget.html(`
|
||||
<h3>${note.title}</h3>
|
||||
<p>Type: ${note.type}</p>
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `refreshWithNote` method is automatically called whenever the user switches to a different note.
|
||||
|
||||
### Right Panel Widget
|
||||
|
||||
For widgets in the sidebar, extend RightPanelWidget:
|
||||
|
||||
```javascript
|
||||
class StatsWidget extends RightPanelWidget {
|
||||
get widgetTitle() { return "Statistics"; }
|
||||
|
||||
async doRenderBody() {
|
||||
this.$body.html('<div class="stats">Loading...</div>');
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const content = await note.getContent();
|
||||
const words = content.split(/\s+/).length;
|
||||
this.$body.find('.stats').text(`Words: ${words}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Lifecycle
|
||||
|
||||
Widgets go through three main phases:
|
||||
|
||||
**Initialization**: The `doRender()` method creates your widget's HTML structure. This happens once when the widget is first displayed.
|
||||
|
||||
**Updates**: The `refresh()` or `refreshWithNote()` methods update your widget's content. These are called when data changes or the user switches notes.
|
||||
|
||||
**Cleanup**: If your widget creates timers or external connections, override `cleanup()` to properly dispose of them.
|
||||
|
||||
## Handling Events
|
||||
|
||||
Widgets automatically subscribe to events based on method names. Simply define a method ending with "Event" to handle that event:
|
||||
|
||||
```javascript
|
||||
class ReactiveWidget extends NoteContextAwareWidget {
|
||||
// Triggered when note content changes
|
||||
async noteContentChangedEvent({ noteId }) {
|
||||
if (this.noteId === noteId) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Triggered when user switches notes
|
||||
async noteSwitchedEvent() {
|
||||
console.log('Switched to:', this.noteId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Common events include `noteSwitched`, `noteContentChanged`, and `entitiesReloaded`. The event system ensures your widget stays synchronized with Trilium's state.
|
||||
|
||||
## State Management
|
||||
|
||||
### Local State
|
||||
|
||||
Store widget-specific state in instance properties:
|
||||
|
||||
```typescript
|
||||
class StatefulWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.isExpanded = false;
|
||||
this.cachedData = null;
|
||||
}
|
||||
|
||||
toggleExpanded() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
this.$widget.toggleClass('expanded', this.isExpanded);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Persistent State
|
||||
|
||||
Use options or attributes for persistent state:
|
||||
|
||||
```typescript
|
||||
class PersistentWidget extends NoteContextAwareWidget {
|
||||
async saveState(state) {
|
||||
await server.put('options', {
|
||||
name: 'widgetState',
|
||||
value: JSON.stringify(state)
|
||||
});
|
||||
}
|
||||
|
||||
async loadState() {
|
||||
const option = await server.get('options/widgetState');
|
||||
return option ? JSON.parse(option.value) : {};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing Trilium APIs
|
||||
|
||||
### Frontend Services
|
||||
|
||||
```typescript
|
||||
import froca from "../services/froca.js";
|
||||
import server from "../services/server.js";
|
||||
import linkService from "../services/link.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
|
||||
class ApiWidget extends NoteContextAwareWidget {
|
||||
async doRenderBody() {
|
||||
// Access notes
|
||||
const note = await froca.getNote(this.noteId);
|
||||
|
||||
// Get attributes
|
||||
const attributes = note.getAttributes();
|
||||
|
||||
// Create links
|
||||
const $link = await linkService.createLink(note.noteId);
|
||||
|
||||
// Show notifications
|
||||
toastService.showMessage("Widget loaded");
|
||||
|
||||
// Open dialogs
|
||||
const result = await dialogService.confirm("Continue?");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Server Communication
|
||||
|
||||
```typescript
|
||||
class ServerWidget extends BasicWidget {
|
||||
async loadData() {
|
||||
// GET request
|
||||
const data = await server.get('custom-api/data');
|
||||
|
||||
// POST request
|
||||
const result = await server.post('custom-api/process', {
|
||||
noteId: this.noteId,
|
||||
action: 'analyze'
|
||||
});
|
||||
|
||||
// PUT request
|
||||
await server.put(`notes/${this.noteId}`, {
|
||||
title: 'Updated Title'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Styling Widgets
|
||||
|
||||
### Inline Styles
|
||||
|
||||
```typescript
|
||||
class StyledWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>');
|
||||
this.css('padding', '10px')
|
||||
.css('background-color', '#f0f0f0')
|
||||
.css('border-radius', '4px');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Classes
|
||||
|
||||
```typescript
|
||||
class ClassedWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>');
|
||||
this.class('custom-widget')
|
||||
.class('bordered');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Blocks
|
||||
|
||||
```typescript
|
||||
class CSSBlockWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="my-widget">Content</div>');
|
||||
|
||||
this.cssBlock(`
|
||||
.my-widget {
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.my-widget:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
class LazyWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.dataLoaded = false;
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
if (!this.isVisible()) {
|
||||
return; // Don't load if not visible
|
||||
}
|
||||
|
||||
if (!this.dataLoaded) {
|
||||
await this.loadExpensiveData();
|
||||
this.dataLoaded = true;
|
||||
}
|
||||
|
||||
this.updateDisplay();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debouncing Updates
|
||||
|
||||
```typescript
|
||||
import SpacedUpdate from "../services/spaced_update.js";
|
||||
|
||||
class DebouncedWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.performUpdate();
|
||||
}, 500); // 500ms delay
|
||||
}
|
||||
|
||||
async handleInput(value) {
|
||||
await this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
```typescript
|
||||
class CachedWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
async getProcessedData(noteId) {
|
||||
if (!this.cache.has(noteId)) {
|
||||
const data = await this.processExpensiveOperation(noteId);
|
||||
this.cache.set(noteId, data);
|
||||
}
|
||||
return this.cache.get(noteId);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Widgets
|
||||
|
||||
### Console Logging
|
||||
|
||||
```typescript
|
||||
class DebugWidget extends BasicWidget {
|
||||
doRender() {
|
||||
console.log('Widget rendering', this.componentId);
|
||||
console.time('render');
|
||||
|
||||
this.$widget = $('<div>');
|
||||
|
||||
console.timeEnd('render');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
class SafeWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note) {
|
||||
try {
|
||||
await this.riskyOperation();
|
||||
} catch (error) {
|
||||
console.error('Widget error:', error);
|
||||
this.logRenderingError(error);
|
||||
this.$widget.html('<div class="error">Failed to load</div>');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Development Tools
|
||||
|
||||
```typescript
|
||||
class DevWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>');
|
||||
|
||||
// Add debug information in development
|
||||
if (window.glob.isDev) {
|
||||
this.$widget.attr('data-debug', 'true');
|
||||
this.$widget.append(`
|
||||
<div class="debug-info">
|
||||
Component ID: ${this.componentId}
|
||||
Position: ${this.position}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example: Note Statistics Widget
|
||||
|
||||
Here's a complete example implementing a custom note statistics widget:
|
||||
|
||||
```typescript
|
||||
import RightPanelWidget from "../widgets/right_panel_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import froca from "../services/froca.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import SpacedUpdate from "../services/spaced_update.js";
|
||||
|
||||
class NoteStatisticsWidget extends RightPanelWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Initialize state
|
||||
this.statistics = {
|
||||
words: 0,
|
||||
characters: 0,
|
||||
paragraphs: 0,
|
||||
readingTime: 0,
|
||||
links: 0,
|
||||
images: 0
|
||||
};
|
||||
|
||||
// Debounce updates for performance
|
||||
this.spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.calculateStatistics();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
get widgetTitle() {
|
||||
return "Note Statistics";
|
||||
}
|
||||
|
||||
get help() {
|
||||
return {
|
||||
title: "Note Statistics",
|
||||
text: "Displays various statistics about the current note including word count, reading time, and more."
|
||||
};
|
||||
}
|
||||
|
||||
async doRenderBody() {
|
||||
this.$body.html(`
|
||||
<div class="note-statistics">
|
||||
<div class="stat-group">
|
||||
<h5>Content</h5>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Words:</span>
|
||||
<span class="stat-value" data-stat="words">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Characters:</span>
|
||||
<span class="stat-value" data-stat="characters">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Paragraphs:</span>
|
||||
<span class="stat-value" data-stat="paragraphs">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-group">
|
||||
<h5>Reading</h5>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Reading time:</span>
|
||||
<span class="stat-value" data-stat="readingTime">0 min</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-group">
|
||||
<h5>Elements</h5>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Links:</span>
|
||||
<span class="stat-value" data-stat="links">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Images:</span>
|
||||
<span class="stat-value" data-stat="images">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-actions">
|
||||
<button class="btn btn-sm refresh-stats">Refresh</button>
|
||||
<button class="btn btn-sm export-stats">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.cssBlock(`
|
||||
.note-statistics {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stat-group {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.stat-group:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-group h5 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--muted-text-color);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.stat-actions {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-actions .btn {
|
||||
flex: 1;
|
||||
}
|
||||
`);
|
||||
|
||||
// Bind events
|
||||
this.$body.on('click', '.refresh-stats', () => this.handleRefresh());
|
||||
this.$body.on('click', '.export-stats', () => this.handleExport());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
if (!note) {
|
||||
this.clearStatistics();
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule statistics calculation
|
||||
await this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
async calculateStatistics() {
|
||||
try {
|
||||
const note = this.note;
|
||||
if (!note) return;
|
||||
|
||||
const content = await note.getContent();
|
||||
|
||||
if (note.type === 'text') {
|
||||
// Parse HTML content
|
||||
const $content = $('<div>').html(content);
|
||||
const textContent = $content.text();
|
||||
|
||||
// Calculate statistics
|
||||
this.statistics.words = this.countWords(textContent);
|
||||
this.statistics.characters = textContent.length;
|
||||
this.statistics.paragraphs = $content.find('p').length;
|
||||
this.statistics.readingTime = Math.ceil(this.statistics.words / 200);
|
||||
this.statistics.links = $content.find('a').length;
|
||||
this.statistics.images = $content.find('img').length;
|
||||
} else if (note.type === 'code') {
|
||||
// For code notes, count lines and characters
|
||||
const lines = content.split('\n');
|
||||
this.statistics.words = lines.length; // Show lines instead of words
|
||||
this.statistics.characters = content.length;
|
||||
this.statistics.paragraphs = 0;
|
||||
this.statistics.readingTime = 0;
|
||||
this.statistics.links = 0;
|
||||
this.statistics.images = 0;
|
||||
}
|
||||
|
||||
this.updateDisplay();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to calculate statistics:', error);
|
||||
toastService.showError("Failed to calculate statistics");
|
||||
}
|
||||
}
|
||||
|
||||
countWords(text) {
|
||||
const words = text.match(/\b\w+\b/g);
|
||||
return words ? words.length : 0;
|
||||
}
|
||||
|
||||
clearStatistics() {
|
||||
this.statistics = {
|
||||
words: 0,
|
||||
characters: 0,
|
||||
paragraphs: 0,
|
||||
readingTime: 0,
|
||||
links: 0,
|
||||
images: 0
|
||||
};
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
this.$body.find('[data-stat="words"]').text(this.statistics.words);
|
||||
this.$body.find('[data-stat="characters"]').text(this.statistics.characters);
|
||||
this.$body.find('[data-stat="paragraphs"]').text(this.statistics.paragraphs);
|
||||
this.$body.find('[data-stat="readingTime"]').text(`${this.statistics.readingTime} min`);
|
||||
this.$body.find('[data-stat="links"]').text(this.statistics.links);
|
||||
this.$body.find('[data-stat="images"]').text(this.statistics.images);
|
||||
}
|
||||
|
||||
async handleRefresh() {
|
||||
await this.calculateStatistics();
|
||||
toastService.showMessage("Statistics refreshed");
|
||||
}
|
||||
|
||||
async handleExport() {
|
||||
const note = this.note;
|
||||
if (!note) return;
|
||||
|
||||
const exportData = {
|
||||
noteId: note.noteId,
|
||||
title: note.title,
|
||||
statistics: this.statistics,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Create a CSV
|
||||
const csv = [
|
||||
'Metric,Value',
|
||||
`Words,${this.statistics.words}`,
|
||||
`Characters,${this.statistics.characters}`,
|
||||
`Paragraphs,${this.statistics.paragraphs}`,
|
||||
`Reading Time,${this.statistics.readingTime} minutes`,
|
||||
`Links,${this.statistics.links}`,
|
||||
`Images,${this.statistics.images}`
|
||||
].join('\n');
|
||||
|
||||
// Download CSV
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `statistics-${note.noteId}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toastService.showMessage("Statistics exported");
|
||||
}
|
||||
|
||||
async noteContentChangedEvent({ noteId }) {
|
||||
if (this.noteId === noteId) {
|
||||
await this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.$body.off('click');
|
||||
this.spacedUpdate = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteStatisticsWidget;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1\. Memory Management
|
||||
|
||||
* Clean up event listeners in `cleanup()`
|
||||
* Clear caches and timers when widget is destroyed
|
||||
* Avoid circular references
|
||||
|
||||
### 2\. Performance
|
||||
|
||||
* Use debouncing for frequent updates
|
||||
* Implement lazy loading for expensive operations
|
||||
* Cache computed values when appropriate
|
||||
|
||||
### 3\. Error Handling
|
||||
|
||||
* Always wrap async operations in try-catch
|
||||
* Provide user feedback for errors
|
||||
* Log errors for debugging
|
||||
|
||||
### 4\. User Experience
|
||||
|
||||
* Show loading states for async operations
|
||||
* Provide clear error messages
|
||||
* Ensure widgets are responsive
|
||||
|
||||
### 5\. Code Organization
|
||||
|
||||
* Keep widgets focused on a single responsibility
|
||||
* Extract reusable logic into services
|
||||
* Use composition over inheritance when possible
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Widget Not Rendering
|
||||
|
||||
* Check `doRender()` creates `this.$widget`
|
||||
* Verify widget is properly registered
|
||||
* Check console for errors
|
||||
|
||||
### Events Not Firing
|
||||
|
||||
* Ensure event method name matches pattern: `${eventName}Event`
|
||||
* Check event is being triggered
|
||||
* Verify widget is active/visible
|
||||
|
||||
### State Not Persisting
|
||||
|
||||
* Use options or attributes for persistence
|
||||
* Check save operations complete successfully
|
||||
* Verify data serialization
|
||||
|
||||
### Performance Issues
|
||||
|
||||
* Profile with browser dev tools
|
||||
* Implement caching and debouncing
|
||||
* Optimize DOM operations
|
||||
|
||||
## Next Steps
|
||||
|
||||
* Explore existing widgets in `/apps/client/src/widgets/` for examples
|
||||
* Review the Frontend Script API documentation
|
||||
* Join the Trilium community for support and sharing widgets
|
||||
1042
docs/Developer Guide/Developer Guide/Plugin Development/Frontend Script Development.md
vendored
Normal file
1042
docs/Developer Guide/Developer Guide/Plugin Development/Frontend Script Development.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1173
docs/Developer Guide/Developer Guide/Plugin Development/Theme Development Guide.md
vendored
Normal file
1173
docs/Developer Guide/Developer Guide/Plugin Development/Theme Development Guide.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user