mirror of
https://github.com/zadam/trilium.git
synced 2025-12-11 01:44:24 +01:00
1867 lines
46 KiB
Markdown
Vendored
1867 lines
46 KiB
Markdown
Vendored
# Internal API Reference
|
|
|
|
## Table of Contents
|
|
1. [Introduction](#introduction)
|
|
2. [Authentication and Session Management](#authentication-and-session-management)
|
|
3. [Core API Endpoints](#core-api-endpoints)
|
|
4. [WebSocket Real-time Updates](#websocket-real-time-updates)
|
|
5. [File Operations](#file-operations)
|
|
6. [Import/Export Operations](#import-export-operations)
|
|
7. [Synchronization API](#synchronization-api)
|
|
8. [When to Use Internal vs ETAPI](#when-to-use-internal-vs-etapi)
|
|
9. [Security Considerations](#security-considerations)
|
|
|
|
## Introduction
|
|
|
|
The Internal API is the primary interface used by the Trilium Notes client application to communicate with the server. While powerful and feature-complete, this API is primarily designed for internal use.
|
|
|
|
### Important Notice
|
|
**For external integrations, please use [ETAPI](./ETAPI%20Complete%20Guide.md) instead.** The Internal API:
|
|
- May change between versions without notice
|
|
- Requires session-based authentication with CSRF protection
|
|
- Is tightly coupled with the frontend application
|
|
- Has limited documentation and stability guarantees
|
|
|
|
### Base URL
|
|
```
|
|
http://localhost:8080/api
|
|
```
|
|
|
|
### Key Characteristics
|
|
- Session-based authentication with cookies
|
|
- CSRF token protection for state-changing operations
|
|
- WebSocket support for real-time updates
|
|
- Full feature parity with the Trilium UI
|
|
- Complex request/response formats optimized for the client
|
|
|
|
## Authentication and Session Management
|
|
|
|
### Password Login
|
|
**POST** `/api/login`
|
|
|
|
Authenticates user with password and creates a session.
|
|
|
|
**Request:**
|
|
```javascript
|
|
const formData = new URLSearchParams();
|
|
formData.append('password', 'your-password');
|
|
|
|
const response = await fetch('http://localhost:8080/api/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: formData,
|
|
credentials: 'include' // Important for cookie handling
|
|
});
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Login successful"
|
|
}
|
|
```
|
|
|
|
The server sets a session cookie (`trilium.sid`) that must be included in subsequent requests.
|
|
|
|
### TOTP Authentication (2FA)
|
|
If 2FA is enabled, include the TOTP token:
|
|
|
|
```javascript
|
|
formData.append('password', 'your-password');
|
|
formData.append('totpToken', '123456');
|
|
```
|
|
|
|
### Token Authentication
|
|
**POST** `/api/login/token`
|
|
|
|
Generate an API token for programmatic access:
|
|
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/login/token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
password: 'your-password',
|
|
tokenName: 'My Integration'
|
|
})
|
|
});
|
|
|
|
const { authToken } = await response.json();
|
|
// Use this token in Authorization header for future requests
|
|
```
|
|
|
|
### Protected Session
|
|
**POST** `/api/login/protected`
|
|
|
|
Enter protected session to access encrypted notes:
|
|
|
|
```javascript
|
|
await fetch('http://localhost:8080/api/login/protected', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
password: 'your-password'
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
### Logout
|
|
**POST** `/api/logout`
|
|
|
|
```javascript
|
|
await fetch('http://localhost:8080/api/logout', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
## Core API Endpoints
|
|
|
|
### Notes
|
|
|
|
#### Get Note
|
|
**GET** `/api/notes/{noteId}`
|
|
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/notes/root', {
|
|
credentials: 'include'
|
|
});
|
|
|
|
const note = await response.json();
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"noteId": "root",
|
|
"title": "Trilium Notes",
|
|
"type": "text",
|
|
"mime": "text/html",
|
|
"isProtected": false,
|
|
"isDeleted": false,
|
|
"dateCreated": "2024-01-01 00:00:00.000+0000",
|
|
"dateModified": "2024-01-15 10:30:00.000+0000",
|
|
"utcDateCreated": "2024-01-01 00:00:00.000Z",
|
|
"utcDateModified": "2024-01-15 10:30:00.000Z",
|
|
"parentBranches": [
|
|
{
|
|
"branchId": "root_root",
|
|
"parentNoteId": "none",
|
|
"prefix": null,
|
|
"notePosition": 10
|
|
}
|
|
],
|
|
"attributes": [],
|
|
"cssClass": "",
|
|
"iconClass": "bx bx-folder"
|
|
}
|
|
```
|
|
|
|
#### Create Note
|
|
**POST** `/api/notes/{parentNoteId}/children`
|
|
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/notes/root/children', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
title: 'New Note',
|
|
type: 'text',
|
|
content: '<p>Note content</p>',
|
|
isProtected: false
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
|
|
const { note, branch } = await response.json();
|
|
```
|
|
|
|
#### Update Note
|
|
**PUT** `/api/notes/{noteId}`
|
|
|
|
```javascript
|
|
await fetch(`http://localhost:8080/api/notes/${noteId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
title: 'Updated Title',
|
|
type: 'text',
|
|
mime: 'text/html'
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
#### Delete Note
|
|
**DELETE** `/api/notes/{noteId}`
|
|
|
|
```javascript
|
|
await fetch(`http://localhost:8080/api/notes/${noteId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
#### Get Note Content
|
|
**GET** `/api/notes/{noteId}/content`
|
|
|
|
Returns the actual content of the note:
|
|
|
|
```javascript
|
|
const response = await fetch(`http://localhost:8080/api/notes/${noteId}/content`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
const content = await response.text();
|
|
```
|
|
|
|
#### Save Note Content
|
|
**PUT** `/api/notes/{noteId}/content`
|
|
|
|
```javascript
|
|
await fetch(`http://localhost:8080/api/notes/${noteId}/content`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'text/html',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: '<p>Updated content</p>',
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
### Tree Operations
|
|
|
|
#### Get Branch
|
|
**GET** `/api/branches/{branchId}`
|
|
|
|
```javascript
|
|
const branch = await fetch(`http://localhost:8080/api/branches/${branchId}`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
#### Move Note
|
|
**PUT** `/api/branches/{branchId}/move`
|
|
|
|
```javascript
|
|
await fetch(`http://localhost:8080/api/branches/${branchId}/move`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
parentNoteId: 'newParentId',
|
|
beforeNoteId: 'siblingNoteId' // optional, for positioning
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
#### Clone Note
|
|
**POST** `/api/notes/{noteId}/clone`
|
|
|
|
```javascript
|
|
const response = await fetch(`http://localhost:8080/api/notes/${noteId}/clone`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
parentNoteId: 'targetParentId',
|
|
prefix: 'Copy of '
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
#### Sort Child Notes
|
|
**PUT** `/api/notes/{noteId}/sort-children`
|
|
|
|
```javascript
|
|
await fetch(`http://localhost:8080/api/notes/${noteId}/sort-children`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
sortBy: 'title', // or 'dateCreated', 'dateModified'
|
|
reverse: false
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
### Attributes
|
|
|
|
#### Create Attribute
|
|
**POST** `/api/notes/{noteId}/attributes`
|
|
|
|
```javascript
|
|
const response = await fetch(`http://localhost:8080/api/notes/${noteId}/attributes`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
type: 'label',
|
|
name: 'todo',
|
|
value: '',
|
|
isInheritable: false
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
#### Update Attribute
|
|
**PUT** `/api/attributes/{attributeId}`
|
|
|
|
```javascript
|
|
await fetch(`http://localhost:8080/api/attributes/${attributeId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
value: 'updated value'
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
#### Delete Attribute
|
|
**DELETE** `/api/attributes/{attributeId}`
|
|
|
|
```javascript
|
|
await fetch(`http://localhost:8080/api/attributes/${attributeId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
### Search
|
|
|
|
#### Search Notes
|
|
**GET** `/api/search`
|
|
|
|
```javascript
|
|
const params = new URLSearchParams({
|
|
query: '#todo OR #task',
|
|
fastSearch: 'false',
|
|
includeArchivedNotes: 'false',
|
|
ancestorNoteId: 'root',
|
|
orderBy: 'relevancy',
|
|
orderDirection: 'desc',
|
|
limit: '50'
|
|
});
|
|
|
|
const response = await fetch(`http://localhost:8080/api/search?${params}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
const { results } = await response.json();
|
|
```
|
|
|
|
#### Search Note Map
|
|
**GET** `/api/search-note-map`
|
|
|
|
Returns hierarchical structure of search results:
|
|
|
|
```javascript
|
|
const params = new URLSearchParams({
|
|
query: 'project',
|
|
maxDepth: '3'
|
|
});
|
|
|
|
const noteMap = await fetch(`http://localhost:8080/api/search-note-map?${params}`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
### Revisions
|
|
|
|
#### Get Note Revisions
|
|
**GET** `/api/notes/{noteId}/revisions`
|
|
|
|
```javascript
|
|
const revisions = await fetch(`http://localhost:8080/api/notes/${noteId}/revisions`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
#### Get Revision Content
|
|
**GET** `/api/revisions/{revisionId}/content`
|
|
|
|
```javascript
|
|
const content = await fetch(`http://localhost:8080/api/revisions/${revisionId}/content`, {
|
|
credentials: 'include'
|
|
}).then(r => r.text());
|
|
```
|
|
|
|
#### Restore Revision
|
|
**POST** `/api/revisions/{revisionId}/restore`
|
|
|
|
```javascript
|
|
await fetch(`http://localhost:8080/api/revisions/${revisionId}/restore`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
#### Delete Revision
|
|
**DELETE** `/api/revisions/{revisionId}`
|
|
|
|
```javascript
|
|
await fetch(`http://localhost:8080/api/revisions/${revisionId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
## WebSocket Real-time Updates
|
|
|
|
The Internal API provides WebSocket connections for real-time synchronization and updates.
|
|
|
|
### Connection Setup
|
|
|
|
```javascript
|
|
class TriliumWebSocket {
|
|
constructor() {
|
|
this.ws = null;
|
|
this.reconnectInterval = 5000;
|
|
this.shouldReconnect = true;
|
|
}
|
|
|
|
connect() {
|
|
// WebSocket URL same as base URL but with ws:// protocol
|
|
const wsUrl = 'ws://localhost:8080';
|
|
|
|
this.ws = new WebSocket(wsUrl);
|
|
|
|
this.ws.onopen = () => {
|
|
console.log('WebSocket connected');
|
|
this.sendPing();
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
const message = JSON.parse(event.data);
|
|
this.handleMessage(message);
|
|
};
|
|
|
|
this.ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
};
|
|
|
|
this.ws.onclose = () => {
|
|
console.log('WebSocket disconnected');
|
|
if (this.shouldReconnect) {
|
|
setTimeout(() => this.connect(), this.reconnectInterval);
|
|
}
|
|
};
|
|
}
|
|
|
|
handleMessage(message) {
|
|
switch (message.type) {
|
|
case 'sync':
|
|
this.handleSync(message.data);
|
|
break;
|
|
case 'entity-changes':
|
|
this.handleEntityChanges(message.data);
|
|
break;
|
|
case 'refresh-tree':
|
|
this.refreshTree();
|
|
break;
|
|
case 'create-note':
|
|
this.handleNoteCreated(message.data);
|
|
break;
|
|
case 'update-note':
|
|
this.handleNoteUpdated(message.data);
|
|
break;
|
|
case 'delete-note':
|
|
this.handleNoteDeleted(message.data);
|
|
break;
|
|
default:
|
|
console.log('Unknown message type:', message.type);
|
|
}
|
|
}
|
|
|
|
sendPing() {
|
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
setTimeout(() => this.sendPing(), 30000); // Ping every 30 seconds
|
|
}
|
|
}
|
|
|
|
send(type, data) {
|
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify({ type, data }));
|
|
}
|
|
}
|
|
|
|
handleSync(data) {
|
|
// Handle synchronization data
|
|
console.log('Sync data received:', data);
|
|
}
|
|
|
|
handleEntityChanges(changes) {
|
|
// Handle entity change notifications
|
|
changes.forEach(change => {
|
|
console.log(`Entity ${change.entityName} ${change.entityId} changed`);
|
|
});
|
|
}
|
|
|
|
refreshTree() {
|
|
// Refresh the note tree UI
|
|
console.log('Tree refresh requested');
|
|
}
|
|
|
|
handleNoteCreated(note) {
|
|
console.log('Note created:', note);
|
|
}
|
|
|
|
handleNoteUpdated(note) {
|
|
console.log('Note updated:', note);
|
|
}
|
|
|
|
handleNoteDeleted(noteId) {
|
|
console.log('Note deleted:', noteId);
|
|
}
|
|
|
|
disconnect() {
|
|
this.shouldReconnect = false;
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const ws = new TriliumWebSocket();
|
|
ws.connect();
|
|
|
|
// Send custom message
|
|
ws.send('log-info', { info: 'Client started' });
|
|
|
|
// Clean up on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
ws.disconnect();
|
|
});
|
|
```
|
|
|
|
### Message Types
|
|
|
|
#### Incoming Messages
|
|
|
|
| Type | Description | Data Format |
|
|
|------|-------------|-------------|
|
|
| `sync` | Synchronization data | `{ entityChanges: [], lastSyncedPush: number }` |
|
|
| `entity-changes` | Entity modifications | `[{ entityName, entityId, action }]` |
|
|
| `refresh-tree` | Tree structure changed | None |
|
|
| `create-note` | Note created | Note object |
|
|
| `update-note` | Note updated | Note object |
|
|
| `delete-note` | Note deleted | `{ noteId }` |
|
|
| `frontend-script` | Execute frontend script | `{ script, params }` |
|
|
|
|
#### Outgoing Messages
|
|
|
|
| Type | Description | Data Format |
|
|
|------|-------------|-------------|
|
|
| `ping` | Keep connection alive | None |
|
|
| `log-error` | Log client error | `{ error, stack }` |
|
|
| `log-info` | Log client info | `{ info }` |
|
|
|
|
### Real-time Collaboration Example
|
|
|
|
```javascript
|
|
class CollaborativeEditor {
|
|
constructor(noteId) {
|
|
this.noteId = noteId;
|
|
this.ws = new TriliumWebSocket();
|
|
this.content = '';
|
|
this.lastSaved = '';
|
|
|
|
this.ws.handleNoteUpdated = (note) => {
|
|
if (note.noteId === this.noteId) {
|
|
this.handleRemoteUpdate(note);
|
|
}
|
|
};
|
|
}
|
|
|
|
async loadNote() {
|
|
const response = await fetch(`/api/notes/${this.noteId}/content`, {
|
|
credentials: 'include'
|
|
});
|
|
this.content = await response.text();
|
|
this.lastSaved = this.content;
|
|
}
|
|
|
|
handleRemoteUpdate(note) {
|
|
// Check if the update is from another client
|
|
if (this.content !== this.lastSaved) {
|
|
// Show conflict resolution UI
|
|
this.showConflictDialog(note);
|
|
} else {
|
|
// Apply remote changes
|
|
this.loadNote();
|
|
}
|
|
}
|
|
|
|
async saveContent(content) {
|
|
this.content = content;
|
|
|
|
await fetch(`/api/notes/${this.noteId}/content`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'text/html',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: content,
|
|
credentials: 'include'
|
|
});
|
|
|
|
this.lastSaved = content;
|
|
}
|
|
|
|
showConflictDialog(remoteNote) {
|
|
// Implementation of conflict resolution UI
|
|
console.log('Conflict detected with remote changes');
|
|
}
|
|
}
|
|
```
|
|
|
|
## File Operations
|
|
|
|
### Upload File
|
|
**POST** `/api/notes/{noteId}/attachments/upload`
|
|
|
|
```javascript
|
|
const formData = new FormData();
|
|
formData.append('file', fileInput.files[0]);
|
|
|
|
const response = await fetch(`/api/notes/${noteId}/attachments/upload`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: formData,
|
|
credentials: 'include'
|
|
});
|
|
|
|
const attachment = await response.json();
|
|
```
|
|
|
|
### Download Attachment
|
|
**GET** `/api/attachments/{attachmentId}/download`
|
|
|
|
```javascript
|
|
const response = await fetch(`/api/attachments/${attachmentId}/download`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'attachment.pdf';
|
|
a.click();
|
|
```
|
|
|
|
### Upload Image
|
|
**POST** `/api/images/upload`
|
|
|
|
```javascript
|
|
const formData = new FormData();
|
|
formData.append('image', imageFile);
|
|
formData.append('noteId', noteId);
|
|
|
|
const response = await fetch('/api/images/upload', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: formData,
|
|
credentials: 'include'
|
|
});
|
|
|
|
const { url, noteId: imageNoteId } = await response.json();
|
|
```
|
|
|
|
## Import/Export Operations
|
|
|
|
### Import ZIP
|
|
**POST** `/api/import`
|
|
|
|
```javascript
|
|
const formData = new FormData();
|
|
formData.append('file', zipFile);
|
|
formData.append('parentNoteId', 'root');
|
|
|
|
const response = await fetch('/api/import', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: formData,
|
|
credentials: 'include'
|
|
});
|
|
|
|
const result = await response.json();
|
|
```
|
|
|
|
### Export Subtree
|
|
**GET** `/api/notes/{noteId}/export`
|
|
|
|
```javascript
|
|
const params = new URLSearchParams({
|
|
format: 'html', // or 'markdown'
|
|
exportRevisions: 'true'
|
|
});
|
|
|
|
const response = await fetch(`/api/notes/${noteId}/export?${params}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'export.zip';
|
|
a.click();
|
|
```
|
|
|
|
### Import Markdown
|
|
**POST** `/api/import/markdown`
|
|
|
|
```javascript
|
|
const response = await fetch('/api/import/markdown', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
parentNoteId: 'root',
|
|
content: '# Markdown Content\n\nParagraph text...',
|
|
title: 'Imported from Markdown'
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
### Export as PDF
|
|
**GET** `/api/notes/{noteId}/export/pdf`
|
|
|
|
```javascript
|
|
const response = await fetch(`/api/notes/${noteId}/export/pdf`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
window.open(url, '_blank');
|
|
```
|
|
|
|
## Synchronization API
|
|
|
|
### Get Sync Status
|
|
**GET** `/api/sync/status`
|
|
|
|
```javascript
|
|
const status = await fetch('/api/sync/status', {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
|
|
console.log('Sync enabled:', status.syncEnabled);
|
|
console.log('Last sync:', status.lastSyncedPush);
|
|
```
|
|
|
|
### Force Sync
|
|
**POST** `/api/sync/now`
|
|
|
|
```javascript
|
|
await fetch('/api/sync/now', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
### Get Sync Log
|
|
**GET** `/api/sync/log`
|
|
|
|
```javascript
|
|
const log = await fetch('/api/sync/log', {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
|
|
log.forEach(entry => {
|
|
console.log(`${entry.date}: ${entry.message}`);
|
|
});
|
|
```
|
|
|
|
## Script Execution
|
|
|
|
### Execute Script
|
|
**POST** `/api/script/run`
|
|
|
|
```javascript
|
|
const response = await fetch('/api/script/run', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
script: `
|
|
const note = await api.getNote('root');
|
|
return { title: note.title, children: note.children.length };
|
|
`,
|
|
params: {}
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
|
|
const result = await response.json();
|
|
```
|
|
|
|
### Execute Note Script
|
|
**POST** `/api/notes/{noteId}/run`
|
|
|
|
Run a script note:
|
|
|
|
```javascript
|
|
const response = await fetch(`/api/notes/${scriptNoteId}/run`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
params: {
|
|
targetNoteId: 'someNoteId'
|
|
}
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
|
|
const result = await response.json();
|
|
```
|
|
|
|
## Special Features
|
|
|
|
### Calendar API
|
|
|
|
#### Get Day Note
|
|
**GET** `/api/calendar/days/{date}`
|
|
|
|
```javascript
|
|
const date = '2024-01-15';
|
|
const dayNote = await fetch(`/api/calendar/days/${date}`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
#### Get Week Note
|
|
**GET** `/api/calendar/weeks/{date}`
|
|
|
|
```javascript
|
|
const weekNote = await fetch(`/api/calendar/weeks/2024-01-15`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
#### Get Month Note
|
|
**GET** `/api/calendar/months/{month}`
|
|
|
|
```javascript
|
|
const monthNote = await fetch(`/api/calendar/months/2024-01`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
### Inbox Note
|
|
**GET** `/api/inbox/{date}`
|
|
|
|
```javascript
|
|
const inboxNote = await fetch(`/api/inbox/2024-01-15`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
### Note Map
|
|
**GET** `/api/notes/{noteId}/map`
|
|
|
|
Get visual map data for a note:
|
|
|
|
```javascript
|
|
const mapData = await fetch(`/api/notes/${noteId}/map`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
|
|
// Returns nodes and links for visualization
|
|
console.log('Nodes:', mapData.nodes);
|
|
console.log('Links:', mapData.links);
|
|
```
|
|
|
|
### Similar Notes
|
|
**GET** `/api/notes/{noteId}/similar`
|
|
|
|
Find notes similar to the given note:
|
|
|
|
```javascript
|
|
const similarNotes = await fetch(`/api/notes/${noteId}/similar`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
## Options and Configuration
|
|
|
|
### Get All Options
|
|
**GET** `/api/options`
|
|
|
|
```javascript
|
|
const options = await fetch('/api/options', {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
### Update Option
|
|
**PUT** `/api/options/{optionName}`
|
|
|
|
```javascript
|
|
await fetch(`/api/options/theme`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
value: 'dark'
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
### Get User Preferences
|
|
**GET** `/api/options/user`
|
|
|
|
```javascript
|
|
const preferences = await fetch('/api/options/user', {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
```
|
|
|
|
## Database Operations
|
|
|
|
### Backup Database
|
|
**POST** `/api/database/backup`
|
|
|
|
```javascript
|
|
const response = await fetch('/api/database/backup', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
backupName: 'manual-backup'
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
|
|
const { backupFile } = await response.json();
|
|
```
|
|
|
|
### Vacuum Database
|
|
**POST** `/api/database/vacuum`
|
|
|
|
```javascript
|
|
await fetch('/api/database/vacuum', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
### Get Database Info
|
|
**GET** `/api/database/info`
|
|
|
|
```javascript
|
|
const info = await fetch('/api/database/info', {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
|
|
console.log('Database size:', info.size);
|
|
console.log('Note count:', info.noteCount);
|
|
console.log('Revision count:', info.revisionCount);
|
|
```
|
|
|
|
## When to Use Internal vs ETAPI
|
|
|
|
### Use Internal API When:
|
|
- Building custom Trilium clients
|
|
- Needing WebSocket real-time updates
|
|
- Requiring full feature parity with the UI
|
|
- Working within the Trilium frontend environment
|
|
- Accessing advanced features not available in ETAPI
|
|
|
|
### Use ETAPI When:
|
|
- Building external integrations
|
|
- Creating automation scripts
|
|
- Developing third-party applications
|
|
- Needing stable, documented API
|
|
- Working with different programming languages
|
|
|
|
### Feature Comparison
|
|
|
|
| Feature | Internal API | ETAPI |
|
|
|---------|-------------|--------|
|
|
| **Authentication** | Session/Cookie | Token |
|
|
| **CSRF Protection** | Required | Not needed |
|
|
| **WebSocket** | Yes | No |
|
|
| **Stability** | May change | Stable |
|
|
| **Documentation** | Limited | Comprehensive |
|
|
| **Real-time updates** | Yes | No |
|
|
| **File uploads** | Complex | Simple |
|
|
| **Scripting** | Full support | Limited |
|
|
| **Synchronization** | Yes | No |
|
|
|
|
## Security Considerations
|
|
|
|
### CSRF Protection
|
|
|
|
All state-changing operations require a CSRF token:
|
|
|
|
```javascript
|
|
// Get CSRF token from meta tag or API
|
|
async function getCsrfToken() {
|
|
const response = await fetch('/api/csrf-token', {
|
|
credentials: 'include'
|
|
});
|
|
const { token } = await response.json();
|
|
return token;
|
|
}
|
|
|
|
// Use in requests
|
|
const csrfToken = await getCsrfToken();
|
|
|
|
await fetch('/api/notes', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify(data),
|
|
credentials: 'include'
|
|
});
|
|
```
|
|
|
|
### Session Management
|
|
|
|
```javascript
|
|
class TriliumSession {
|
|
constructor() {
|
|
this.isAuthenticated = false;
|
|
this.csrfToken = null;
|
|
}
|
|
|
|
async login(password) {
|
|
const formData = new URLSearchParams();
|
|
formData.append('password', password);
|
|
|
|
const response = await fetch('/api/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: formData,
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (response.ok) {
|
|
this.isAuthenticated = true;
|
|
this.csrfToken = await this.getCsrfToken();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async getCsrfToken() {
|
|
const response = await fetch('/api/csrf-token', {
|
|
credentials: 'include'
|
|
});
|
|
const { token } = await response.json();
|
|
return token;
|
|
}
|
|
|
|
async request(url, options = {}) {
|
|
if (!this.isAuthenticated) {
|
|
throw new Error('Not authenticated');
|
|
}
|
|
|
|
const headers = {
|
|
...options.headers
|
|
};
|
|
|
|
if (options.method && options.method !== 'GET') {
|
|
headers['X-CSRF-Token'] = this.csrfToken;
|
|
}
|
|
|
|
return fetch(url, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include'
|
|
});
|
|
}
|
|
|
|
async logout() {
|
|
await this.request('/api/logout', { method: 'POST' });
|
|
this.isAuthenticated = false;
|
|
this.csrfToken = null;
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const session = new TriliumSession();
|
|
await session.login('password');
|
|
|
|
// Make authenticated requests
|
|
const notes = await session.request('/api/notes/root').then(r => r.json());
|
|
|
|
// Create note with CSRF protection
|
|
await session.request('/api/notes/root/children', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title: 'New Note', type: 'text' })
|
|
});
|
|
|
|
await session.logout();
|
|
```
|
|
|
|
### Protected Notes
|
|
|
|
Handle encrypted notes properly:
|
|
|
|
```javascript
|
|
class ProtectedNoteHandler {
|
|
constructor(session) {
|
|
this.session = session;
|
|
this.protectedSessionTimeout = null;
|
|
}
|
|
|
|
async enterProtectedSession(password) {
|
|
const response = await this.session.request('/api/login/protected', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ password })
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Protected session expires after inactivity
|
|
this.resetProtectedSessionTimeout();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
resetProtectedSessionTimeout() {
|
|
if (this.protectedSessionTimeout) {
|
|
clearTimeout(this.protectedSessionTimeout);
|
|
}
|
|
|
|
// Assume 5 minute timeout
|
|
this.protectedSessionTimeout = setTimeout(() => {
|
|
console.log('Protected session expired');
|
|
this.onProtectedSessionExpired();
|
|
}, 5 * 60 * 1000);
|
|
}
|
|
|
|
async accessProtectedNote(noteId) {
|
|
try {
|
|
const note = await this.session.request(`/api/notes/${noteId}`)
|
|
.then(r => r.json());
|
|
|
|
if (note.isProtected) {
|
|
// Reset timeout on successful access
|
|
this.resetProtectedSessionTimeout();
|
|
}
|
|
|
|
return note;
|
|
} catch (error) {
|
|
if (error.message.includes('Protected session required')) {
|
|
// Prompt for password
|
|
const password = await this.promptForPassword();
|
|
if (await this.enterProtectedSession(password)) {
|
|
return this.accessProtectedNote(noteId);
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async promptForPassword() {
|
|
// Implementation depends on UI framework
|
|
return prompt('Enter password for protected notes:');
|
|
}
|
|
|
|
onProtectedSessionExpired() {
|
|
// Handle expiration (e.g., show notification, lock UI)
|
|
console.log('Please re-enter password to access protected notes');
|
|
}
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Common Error Responses
|
|
|
|
```javascript
|
|
// 401 Unauthorized
|
|
{
|
|
"status": 401,
|
|
"message": "Authentication required"
|
|
}
|
|
|
|
// 403 Forbidden
|
|
{
|
|
"status": 403,
|
|
"message": "CSRF token validation failed"
|
|
}
|
|
|
|
// 404 Not Found
|
|
{
|
|
"status": 404,
|
|
"message": "Note 'invalidId' not found"
|
|
}
|
|
|
|
// 400 Bad Request
|
|
{
|
|
"status": 400,
|
|
"message": "Invalid note type: 'invalid'"
|
|
}
|
|
|
|
// 500 Internal Server Error
|
|
{
|
|
"status": 500,
|
|
"message": "Database error",
|
|
"stack": "..." // Only in development
|
|
}
|
|
```
|
|
|
|
### Error Handler Implementation
|
|
|
|
```javascript
|
|
class APIErrorHandler {
|
|
async handleResponse(response) {
|
|
if (!response.ok) {
|
|
const error = await this.parseError(response);
|
|
|
|
switch (response.status) {
|
|
case 401:
|
|
this.handleAuthError(error);
|
|
break;
|
|
case 403:
|
|
this.handleForbiddenError(error);
|
|
break;
|
|
case 404:
|
|
this.handleNotFoundError(error);
|
|
break;
|
|
case 400:
|
|
this.handleBadRequestError(error);
|
|
break;
|
|
case 500:
|
|
this.handleServerError(error);
|
|
break;
|
|
default:
|
|
this.handleGenericError(error);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
async parseError(response) {
|
|
try {
|
|
const errorData = await response.json();
|
|
return new APIError(
|
|
response.status,
|
|
errorData.message || response.statusText,
|
|
errorData
|
|
);
|
|
} catch {
|
|
return new APIError(
|
|
response.status,
|
|
response.statusText
|
|
);
|
|
}
|
|
}
|
|
|
|
handleAuthError(error) {
|
|
console.error('Authentication required');
|
|
// Redirect to login
|
|
window.location.href = '/login';
|
|
}
|
|
|
|
handleForbiddenError(error) {
|
|
if (error.message.includes('CSRF')) {
|
|
console.error('CSRF token invalid, refreshing...');
|
|
// Refresh CSRF token
|
|
this.refreshCsrfToken();
|
|
} else {
|
|
console.error('Access forbidden:', error.message);
|
|
}
|
|
}
|
|
|
|
handleNotFoundError(error) {
|
|
console.error('Resource not found:', error.message);
|
|
}
|
|
|
|
handleBadRequestError(error) {
|
|
console.error('Bad request:', error.message);
|
|
}
|
|
|
|
handleServerError(error) {
|
|
console.error('Server error:', error.message);
|
|
// Show user-friendly error message
|
|
this.showErrorNotification('An error occurred. Please try again later.');
|
|
}
|
|
|
|
handleGenericError(error) {
|
|
console.error('API error:', error);
|
|
}
|
|
|
|
showErrorNotification(message) {
|
|
// Implementation depends on UI framework
|
|
alert(message);
|
|
}
|
|
}
|
|
|
|
class APIError extends Error {
|
|
constructor(status, message, data = {}) {
|
|
super(message);
|
|
this.status = status;
|
|
this.data = data;
|
|
this.name = 'APIError';
|
|
}
|
|
}
|
|
```
|
|
|
|
## Performance Optimization
|
|
|
|
### Request Batching
|
|
|
|
```javascript
|
|
class BatchedAPIClient {
|
|
constructor() {
|
|
this.batchQueue = [];
|
|
this.batchTimeout = null;
|
|
this.batchDelay = 50; // ms
|
|
}
|
|
|
|
async batchRequest(request) {
|
|
return new Promise((resolve, reject) => {
|
|
this.batchQueue.push({ request, resolve, reject });
|
|
|
|
if (!this.batchTimeout) {
|
|
this.batchTimeout = setTimeout(() => {
|
|
this.processBatch();
|
|
}, this.batchDelay);
|
|
}
|
|
});
|
|
}
|
|
|
|
async processBatch() {
|
|
const batch = this.batchQueue.splice(0);
|
|
this.batchTimeout = null;
|
|
|
|
if (batch.length === 0) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/batch', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
requests: batch.map(b => b.request)
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
|
|
const results = await response.json();
|
|
|
|
batch.forEach((item, index) => {
|
|
if (results[index].error) {
|
|
item.reject(new Error(results[index].error));
|
|
} else {
|
|
item.resolve(results[index].data);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
batch.forEach(item => item.reject(error));
|
|
}
|
|
}
|
|
|
|
async getNote(noteId) {
|
|
return this.batchRequest({
|
|
method: 'GET',
|
|
url: `/api/notes/${noteId}`
|
|
});
|
|
}
|
|
|
|
async getAttribute(attributeId) {
|
|
return this.batchRequest({
|
|
method: 'GET',
|
|
url: `/api/attributes/${attributeId}`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const client = new BatchedAPIClient();
|
|
|
|
// These requests will be batched
|
|
const [note1, note2, note3] = await Promise.all([
|
|
client.getNote('noteId1'),
|
|
client.getNote('noteId2'),
|
|
client.getNote('noteId3')
|
|
]);
|
|
```
|
|
|
|
### Caching Strategy
|
|
|
|
```javascript
|
|
class CachedAPIClient {
|
|
constructor() {
|
|
this.cache = new Map();
|
|
this.cacheExpiry = new Map();
|
|
this.defaultTTL = 5 * 60 * 1000; // 5 minutes
|
|
}
|
|
|
|
getCacheKey(method, url, params = {}) {
|
|
return `${method}:${url}:${JSON.stringify(params)}`;
|
|
}
|
|
|
|
isExpired(key) {
|
|
const expiry = this.cacheExpiry.get(key);
|
|
return !expiry || Date.now() > expiry;
|
|
}
|
|
|
|
async cachedRequest(method, url, options = {}, ttl = this.defaultTTL) {
|
|
const key = this.getCacheKey(method, url, options.params);
|
|
|
|
if (method === 'GET' && this.cache.has(key) && !this.isExpired(key)) {
|
|
return this.cache.get(key);
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
...options,
|
|
credentials: 'include'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (method === 'GET') {
|
|
this.cache.set(key, data);
|
|
this.cacheExpiry.set(key, Date.now() + ttl);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
invalidate(pattern) {
|
|
for (const key of this.cache.keys()) {
|
|
if (key.includes(pattern)) {
|
|
this.cache.delete(key);
|
|
this.cacheExpiry.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
async getNote(noteId) {
|
|
return this.cachedRequest('GET', `/api/notes/${noteId}`);
|
|
}
|
|
|
|
async updateNote(noteId, data) {
|
|
const result = await fetch(`/api/notes/${noteId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify(data),
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
|
|
// Invalidate cache for this note
|
|
this.invalidate(`/api/notes/${noteId}`);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Advanced Examples
|
|
|
|
### Building a Note Explorer
|
|
|
|
```javascript
|
|
class NoteExplorer {
|
|
constructor() {
|
|
this.currentNote = null;
|
|
this.history = [];
|
|
this.historyIndex = -1;
|
|
}
|
|
|
|
async navigateToNote(noteId) {
|
|
// Add to history
|
|
if (this.historyIndex < this.history.length - 1) {
|
|
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
}
|
|
this.history.push(noteId);
|
|
this.historyIndex++;
|
|
|
|
// Load note
|
|
this.currentNote = await this.loadNoteWithChildren(noteId);
|
|
this.render();
|
|
}
|
|
|
|
async loadNoteWithChildren(noteId) {
|
|
const [note, children] = await Promise.all([
|
|
fetch(`/api/notes/${noteId}`, { credentials: 'include' })
|
|
.then(r => r.json()),
|
|
fetch(`/api/notes/${noteId}/children`, { credentials: 'include' })
|
|
.then(r => r.json())
|
|
]);
|
|
|
|
return { ...note, children };
|
|
}
|
|
|
|
canGoBack() {
|
|
return this.historyIndex > 0;
|
|
}
|
|
|
|
canGoForward() {
|
|
return this.historyIndex < this.history.length - 1;
|
|
}
|
|
|
|
async goBack() {
|
|
if (this.canGoBack()) {
|
|
this.historyIndex--;
|
|
const noteId = this.history[this.historyIndex];
|
|
this.currentNote = await this.loadNoteWithChildren(noteId);
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
async goForward() {
|
|
if (this.canGoForward()) {
|
|
this.historyIndex++;
|
|
const noteId = this.history[this.historyIndex];
|
|
this.currentNote = await this.loadNoteWithChildren(noteId);
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
async searchInSubtree(query) {
|
|
const params = new URLSearchParams({
|
|
query: query,
|
|
ancestorNoteId: this.currentNote.noteId,
|
|
includeArchivedNotes: 'false'
|
|
});
|
|
|
|
const response = await fetch(`/api/search?${params}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
return response.json();
|
|
}
|
|
|
|
async createChildNote(title, content, type = 'text') {
|
|
const response = await fetch(`/api/notes/${this.currentNote.noteId}/children`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': await getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ title, content, type }),
|
|
credentials: 'include'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
// Refresh current note to show new child
|
|
this.currentNote = await this.loadNoteWithChildren(this.currentNote.noteId);
|
|
this.render();
|
|
|
|
return result;
|
|
}
|
|
|
|
render() {
|
|
// Render UI - implementation depends on framework
|
|
console.log('Current note:', this.currentNote.title);
|
|
console.log('Children:', this.currentNote.children.map(c => c.title));
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const explorer = new NoteExplorer();
|
|
await explorer.navigateToNote('root');
|
|
await explorer.createChildNote('New Child', '<p>Content</p>');
|
|
const searchResults = await explorer.searchInSubtree('keyword');
|
|
```
|
|
|
|
### Building a Task Management System
|
|
|
|
```javascript
|
|
class TaskManager {
|
|
constructor() {
|
|
this.taskRootId = null;
|
|
this.csrfToken = null;
|
|
}
|
|
|
|
async initialize() {
|
|
this.csrfToken = await getCsrfToken();
|
|
this.taskRootId = await this.getOrCreateTaskRoot();
|
|
}
|
|
|
|
async getOrCreateTaskRoot() {
|
|
// Search for existing task root
|
|
const searchParams = new URLSearchParams({ query: '#taskRoot' });
|
|
const searchResponse = await fetch(`/api/search?${searchParams}`, {
|
|
credentials: 'include'
|
|
});
|
|
const { results } = await searchResponse.json();
|
|
|
|
if (results.length > 0) {
|
|
return results[0].noteId;
|
|
}
|
|
|
|
// Create task root
|
|
const response = await fetch('/api/notes/root/children', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': this.csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
title: 'Tasks',
|
|
type: 'text',
|
|
content: '<h1>Task Management</h1>'
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
|
|
const { note } = await response.json();
|
|
|
|
// Add taskRoot label
|
|
await this.addLabel(note.noteId, 'taskRoot');
|
|
|
|
return note.noteId;
|
|
}
|
|
|
|
async createTask(title, description, priority = 'medium', dueDate = null) {
|
|
// Create task note
|
|
const response = await fetch(`/api/notes/${this.taskRootId}/children`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': this.csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
title,
|
|
type: 'text',
|
|
content: `<h2>${title}</h2><p>${description}</p>`
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
|
|
const { note } = await response.json();
|
|
|
|
// Add task metadata
|
|
await Promise.all([
|
|
this.addLabel(note.noteId, 'task'),
|
|
this.addLabel(note.noteId, 'status', 'todo'),
|
|
this.addLabel(note.noteId, 'priority', priority),
|
|
dueDate ? this.addLabel(note.noteId, 'dueDate', dueDate) : null
|
|
].filter(Boolean));
|
|
|
|
return note;
|
|
}
|
|
|
|
async addLabel(noteId, name, value = '') {
|
|
await fetch(`/api/notes/${noteId}/attributes`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': this.csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
type: 'label',
|
|
name,
|
|
value,
|
|
isInheritable: false
|
|
}),
|
|
credentials: 'include'
|
|
});
|
|
}
|
|
|
|
async getTasks(status = null, priority = null) {
|
|
let query = '#task';
|
|
if (status) query += ` #status=${status}`;
|
|
if (priority) query += ` #priority=${priority}`;
|
|
|
|
const params = new URLSearchParams({
|
|
query,
|
|
ancestorNoteId: this.taskRootId,
|
|
orderBy: 'dateModified',
|
|
orderDirection: 'desc'
|
|
});
|
|
|
|
const response = await fetch(`/api/search?${params}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
const { results } = await response.json();
|
|
return results;
|
|
}
|
|
|
|
async updateTaskStatus(noteId, newStatus) {
|
|
// Get task attributes
|
|
const note = await fetch(`/api/notes/${noteId}`, {
|
|
credentials: 'include'
|
|
}).then(r => r.json());
|
|
|
|
// Find status attribute
|
|
const statusAttr = note.attributes.find(a => a.name === 'status');
|
|
|
|
if (statusAttr) {
|
|
// Update existing status
|
|
await fetch(`/api/attributes/${statusAttr.attributeId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': this.csrfToken
|
|
},
|
|
body: JSON.stringify({ value: newStatus }),
|
|
credentials: 'include'
|
|
});
|
|
} else {
|
|
// Add status attribute
|
|
await this.addLabel(noteId, 'status', newStatus);
|
|
}
|
|
|
|
// Add completion timestamp if marking as done
|
|
if (newStatus === 'done') {
|
|
const timestamp = new Date().toISOString();
|
|
await this.addLabel(noteId, 'completedAt', timestamp);
|
|
}
|
|
}
|
|
|
|
async getTaskStats() {
|
|
const [todoTasks, inProgressTasks, doneTasks] = await Promise.all([
|
|
this.getTasks('todo'),
|
|
this.getTasks('in-progress'),
|
|
this.getTasks('done')
|
|
]);
|
|
|
|
return {
|
|
todo: todoTasks.length,
|
|
inProgress: inProgressTasks.length,
|
|
done: doneTasks.length,
|
|
total: todoTasks.length + inProgressTasks.length + doneTasks.length
|
|
};
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const taskManager = new TaskManager();
|
|
await taskManager.initialize();
|
|
|
|
// Create tasks
|
|
const task1 = await taskManager.createTask(
|
|
'Review Documentation',
|
|
'Review and update API documentation',
|
|
'high',
|
|
'2024-01-20'
|
|
);
|
|
|
|
const task2 = await taskManager.createTask(
|
|
'Fix Bug #123',
|
|
'Investigate and fix the reported issue',
|
|
'medium'
|
|
);
|
|
|
|
// Get tasks
|
|
const todoTasks = await taskManager.getTasks('todo');
|
|
console.log('Todo tasks:', todoTasks);
|
|
|
|
// Update task status
|
|
await taskManager.updateTaskStatus(task1.noteId, 'in-progress');
|
|
|
|
// Get statistics
|
|
const stats = await taskManager.getTaskStats();
|
|
console.log('Task statistics:', stats);
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
The Internal API provides complete access to Trilium's functionality but should be used with caution due to its complexity and potential for changes. For most external integrations, [ETAPI](./ETAPI%20Complete%20Guide.md) is the recommended choice due to its stability and comprehensive documentation.
|
|
|
|
Key takeaways:
|
|
- Always include CSRF tokens for state-changing operations
|
|
- Handle session management carefully
|
|
- Use WebSocket for real-time updates
|
|
- Implement proper error handling
|
|
- Consider using ETAPI for external integrations
|
|
- Cache responses when appropriate for better performance
|
|
|
|
For additional information, refer to:
|
|
- [ETAPI Complete Guide](./ETAPI%20Complete%20Guide.md)
|
|
- [Script API Cookbook](./Script%20API%20Cookbook.md)
|
|
- [WebSocket API Documentation](./WebSocket%20API.md) |