- ARCHITECTURE.md: main architecture document - api-spec.yaml: full OpenAPI 3.0 spec - folder-structure.md: detailed folder layout - data-format.md: JSON schemas for .meta.json, .library.json, .tag-index.json - env-template.md: environment variables documentation - cli-protocol.md: CLI-to-API communication protocol
414 lines
11 KiB
Markdown
414 lines
11 KiB
Markdown
# SimpleNote - Arquitectura Técnica
|
|
|
|
## 1. Visión General
|
|
|
|
Sistema de gestión de documentos basado en archivos Markdown con API REST. Diseñado para reemplazar Joplin con una arquitectura más simple.
|
|
|
|
```
|
|
┌─────────────────┐ ┌──────────────────┐
|
|
│ simplenote-cli │◄────►│ simplenote-web │
|
|
│ (Commander.js) │ │ (Express + API) │
|
|
└─────────────────┘ └────────┬─────────┘
|
|
│
|
|
┌────────▼─────────┐
|
|
│ File System │
|
|
│ (Markdown + JSON)│
|
|
└───────────────────┘
|
|
```
|
|
|
|
## 2. Componentes
|
|
|
|
### 2.1 simplenote-web
|
|
- **Rol**: API REST + Frontend web
|
|
- **Puerto default**: 3000
|
|
- **Estructura interna**: Express.js con routers modulares
|
|
|
|
### 2.2 simplenote-cli
|
|
- **Rol**: Cliente de línea de comandos
|
|
- **Conexión**: HTTP al API de simplenote-web
|
|
- **Config**: `~/.config/simplenote/config.json`
|
|
|
|
## 3. Arquitectura de Datos
|
|
|
|
### 3.1 Estructura de Librerías (Filesystem)
|
|
|
|
```
|
|
data/ # DATA_ROOT configurable
|
|
└── libraries/
|
|
└── {library-id}/
|
|
├── .library.json # Metadata de librería
|
|
├── documents/
|
|
│ └── {document-id}/
|
|
│ ├── index.md # Contenido
|
|
│ └── .meta.json # Metadata del documento
|
|
└── sub-libraries/
|
|
└── {child-lib-id}/... # Anidamiento recursivo
|
|
```
|
|
|
|
### 3.2 JSON Manifests
|
|
|
|
**`.library.json`** — Metadata de librería:
|
|
```json
|
|
{
|
|
"id": "uuid-v4",
|
|
"name": "Nombre de Librería",
|
|
"parentId": "parent-uuid | null",
|
|
"path": "/libraries/uuid",
|
|
"createdAt": "ISO8601",
|
|
"updatedAt": "ISO8601"
|
|
}
|
|
```
|
|
|
|
**`.meta.json`** — Metadata de documento:
|
|
```json
|
|
{
|
|
"id": "uuid-v4",
|
|
"title": "string",
|
|
"tags": ["tag1", "tag2"],
|
|
"type": "requirement|note|spec|general",
|
|
"status": "draft|approved|implemented",
|
|
"priority": "high|medium|low",
|
|
"createdBy": "agent-id",
|
|
"createdAt": "ISO8601",
|
|
"updatedAt": "ISO8601",
|
|
"libraryId": "uuid"
|
|
}
|
|
```
|
|
|
|
**`.tag-index.json`** — Índice global de tags (en DATA_ROOT):
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"updatedAt": "ISO8601",
|
|
"tags": {
|
|
"backend": ["doc-1", "doc-2"],
|
|
"api": ["doc-1", "doc-3"],
|
|
"auth": ["doc-2"]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.3 Formato de Documento (Markdown)
|
|
|
|
```markdown
|
|
---
|
|
id: REQ-001
|
|
title: Título del Requerimiento
|
|
type: requirement
|
|
priority: high
|
|
status: draft
|
|
tags: [backend, api]
|
|
createdBy: agent-id
|
|
createdAt: 2026-03-28
|
|
---
|
|
|
|
# Título
|
|
|
|
## Descripción
|
|
Descripción del requerimiento.
|
|
|
|
## Criterios de Aceptación
|
|
- [ ] Criterio 1
|
|
- [ ] Criterio 2
|
|
```
|
|
|
|
## 4. API REST
|
|
|
|
### 4.1 Base URL
|
|
```
|
|
/api/v1
|
|
```
|
|
|
|
### 4.2 Autenticación
|
|
- **Método**: Token Bearer en header `Authorization`
|
|
- **Formato**: `Authorization: Bearer <token>`
|
|
- **Admin tokens**: Generados en setup inicial, almacenados en `.auth-tokens.json`
|
|
|
|
### 4.3 Endpoints
|
|
|
|
#### Auth
|
|
| Method | Endpoint | Descripción |
|
|
|--------|----------|-------------|
|
|
| POST | `/api/v1/auth/token` | Generar token (admin) |
|
|
| GET | `/api/v1/auth/verify` | Verificar token válido |
|
|
|
|
#### Documents
|
|
| Method | Endpoint | Descripción |
|
|
|--------|----------|-------------|
|
|
| GET | `/api/v1/documents` | Listar (filtros: tag, library, type) |
|
|
| GET | `/api/v1/documents/:id` | Obtener documento + metadata |
|
|
| POST | `/api/v1/documents` | Crear documento |
|
|
| PUT | `/api/v1/documents/:id` | Actualizar documento |
|
|
| DELETE | `/api/v1/documents/:id` | Eliminar documento |
|
|
| GET | `/api/v1/documents/:id/export` | Exportar como Markdown |
|
|
|
|
#### Libraries
|
|
| Method | Endpoint | Descripción |
|
|
|--------|----------|-------------|
|
|
| GET | `/api/v1/libraries` | Listar librerías raíz |
|
|
| GET | `/api/v1/libraries/:id` | Ver contenido de librería |
|
|
| POST | `/api/v1/libraries` | Crear librería |
|
|
| GET | `/api/v1/libraries/:id/tree` | Árbol completo de sublibrerías |
|
|
| DELETE | `/api/v1/libraries/:id` | Eliminar librería |
|
|
|
|
#### Tags
|
|
| Method | Endpoint | Descripción |
|
|
|--------|----------|-------------|
|
|
| GET | `/api/v1/tags` | Listar todos los tags |
|
|
| GET | `/api/v1/tags/:tag/documents` | Docs con tag específico |
|
|
| POST | `/api/v1/documents/:id/tags` | Agregar tags a documento |
|
|
|
|
## 5. Middleware de Auth
|
|
|
|
### 5.1 Flujo de Tokens
|
|
|
|
1. **Setup inicial**: Se genera admin token (almacenado en `.auth-tokens.json`)
|
|
2. **CLI login**: `simplenote auth login <token>` → almacena token en config local
|
|
3. **Requests**: Token enviado en header `Authorization: Bearer <token>`
|
|
4. **Verificación**: Middleware busca token en `.auth-tokens.json`
|
|
|
|
### 5.2 Implementación
|
|
|
|
```javascript
|
|
// authMiddleware.js
|
|
async function authMiddleware(req, res, next) {
|
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'Token required' });
|
|
}
|
|
|
|
const tokens = await readJSON('.auth-tokens.json');
|
|
if (!tokens.valid.includes(token)) {
|
|
return res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
|
|
req.token = token;
|
|
next();
|
|
}
|
|
```
|
|
|
|
### 5.3 Archivo `.auth-tokens.json`
|
|
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"tokens": [
|
|
{
|
|
"token": "snk_xxxxx",
|
|
"label": "cli-default",
|
|
"createdAt": "ISO8601"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## 6. Estrategia de Indexación de Tags
|
|
|
|
### 6.1write-through
|
|
Cuando se crea/actualiza/elimina un documento:
|
|
1. Se actualiza su `.meta.json` con los tags
|
|
2. Se reconstruye el `.tag-index.json` global
|
|
|
|
### 6.2 Optimización
|
|
- El índice se rebuild en memoria al iniciar (rápido para <10k docs)
|
|
- Para sistemas grandes: rebuild incremental (solo afecta docs modificados)
|
|
- El índice es un archivo JSON plano para búsqueda O(1) por tag
|
|
|
|
### 6.3 API de Búsqueda
|
|
```javascript
|
|
// GET /api/v1/tags/backend
|
|
// → Lee .tag-index.json → returns ["doc-1", "doc-2"]
|
|
|
|
// GET /api/v1/documents?tag=backend
|
|
// → Busca en índice → filtra docs en memoria → retorna
|
|
```
|
|
|
|
## 7. CLI API Client
|
|
|
|
### 7.1 Configuración Local
|
|
```json
|
|
// ~/.config/simplenote/config.json
|
|
{
|
|
"apiUrl": "http://localhost:3000/api/v1",
|
|
"token": "snk_xxxxx",
|
|
"activeLibrary": "default"
|
|
}
|
|
```
|
|
|
|
### 7.2 Cliente HTTP
|
|
|
|
```javascript
|
|
// cli/src/api/client.js
|
|
class SimpleNoteClient {
|
|
constructor(baseUrl, token) {
|
|
this.baseUrl = baseUrl;
|
|
this.token = token;
|
|
}
|
|
|
|
async request(method, path, body) {
|
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
method,
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
throw new Error(err.error || `HTTP ${res.status}`);
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
// Documents
|
|
listDocuments(params) { return this.request('GET', '/documents', null); }
|
|
getDocument(id) { return this.request('GET', `/documents/${id}`); }
|
|
createDocument(data) { return this.request('POST', '/documents', data); }
|
|
updateDocument(id, data) { return this.request('PUT', `/documents/${id}`, data); }
|
|
deleteDocument(id) { return this.request('DELETE', `/documents/${id}`); }
|
|
|
|
// Libraries
|
|
listLibraries() { return this.request('GET', '/libraries'); }
|
|
createLibrary(data) { return this.request('POST', '/libraries', data); }
|
|
|
|
// Tags
|
|
listTags() { return this.request('GET', '/tags'); }
|
|
getTagDocuments(tag) { return this.request('GET', `/tags/${tag}/documents`); }
|
|
|
|
// Auth
|
|
verifyToken() { return this.request('GET', '/auth/verify'); }
|
|
}
|
|
```
|
|
|
|
## 8. Dependencias NPM
|
|
|
|
### simplenote-web
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"express": "^4.18.2",
|
|
"uuid": "^9.0.0",
|
|
"marked": "^11.0.0",
|
|
"gray-matter": "^4.0.3"
|
|
},
|
|
"devDependencies": {
|
|
"nodemon": "^3.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
### simplenote-cli
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"commander": "^11.1.0",
|
|
"axios": "^1.6.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
## 9. Variables de Entorno
|
|
|
|
| Variable | Default | Descripción |
|
|
|----------|---------|-------------|
|
|
| `PORT` | 3000 | Puerto del servidor |
|
|
| `DATA_ROOT` | `./data` | Raíz de documentos |
|
|
| `HOST` | `0.0.0.0` | Host de binding |
|
|
| `LOG_LEVEL` | `info` | Nivel de logging |
|
|
| `CORS_ORIGIN` | `*` | Orígenes CORS permitidos |
|
|
|
|
## 10. Estructura de Archivos del Proyecto
|
|
|
|
```
|
|
simplenote-web/
|
|
├── src/
|
|
│ ├── index.js # Entry point
|
|
│ ├── app.js # Express setup
|
|
│ ├── routes/
|
|
│ │ ├── documents.js
|
|
│ │ ├── libraries.js
|
|
│ │ ├── tags.js
|
|
│ │ └── auth.js
|
|
│ ├── services/
|
|
│ │ ├── documentService.js
|
|
│ │ ├── libraryService.js
|
|
│ │ └── tagService.js
|
|
│ ├── middleware/
|
|
│ │ └── auth.js
|
|
│ ├── utils/
|
|
│ │ ├── markdown.js
|
|
│ │ ├── fsHelper.js
|
|
│ │ └── uuid.js
|
|
│ └── indexers/
|
|
│ └── tagIndexer.js
|
|
├── data/ # .gitkeep
|
|
├── tests/
|
|
├── package.json
|
|
└── README.md
|
|
|
|
simplenote-cli/
|
|
├── src/
|
|
│ ├── index.js # Entry point (Commander)
|
|
│ ├── commands/
|
|
│ │ ├── doc.js
|
|
│ │ ├── lib.js
|
|
│ │ └── tag.js
|
|
│ ├── api/
|
|
│ │ └── client.js
|
|
│ └── config/
|
|
│ └── loader.js
|
|
├── package.json
|
|
└── README.md
|
|
```
|
|
|
|
## 11. Flujo de Operaciones
|
|
|
|
### 11.1 Crear Documento (CLI)
|
|
```
|
|
simplenote doc create --title "REQ-001" --tags "backend,api"
|
|
→ POST /api/v1/documents
|
|
→ Service: genera UUID, crea /data/libraries/{lib}/documents/{id}/index.md
|
|
→ Service: crea /data/libraries/{lib}/documents/{id}/.meta.json
|
|
→ Service: rebuild .tag-index.json
|
|
→ Response: { id, path, tags, ... }
|
|
```
|
|
|
|
### 11.2 Crear Documento (Web)
|
|
```
|
|
Form POST → /api/v1/documents
|
|
→ Mismo flujo que CLI
|
|
→ Web reconstruye lista con filtros
|
|
```
|
|
|
|
### 11.3 Búsqueda por Tag
|
|
```
|
|
GET /api/v1/tags/backend
|
|
→ tagIndexer: lee .tag-index.json
|
|
→ Returns: ["doc-id-1", "doc-id-2"]
|
|
|
|
GET /api/v1/documents?tag=backend
|
|
→ Busca en índice → filtra docs → retorna con metadata
|
|
```
|
|
|
|
## 12. Seguridad
|
|
|
|
- Tokens con prefijo `snk_` para identificación fácil
|
|
- Tokens almacenados en texto plano (sistema multi-usuario confiado)
|
|
- Para producción futura: hash bcrypt + salt
|
|
- CORS configurable para restringir acceso web
|
|
|
|
## 13. Líneas de Código Estimadas
|
|
|
|
| Componente | LOC aprox |
|
|
|------------|-----------|
|
|
| API REST (Express) | ~600 |
|
|
| Services (document, library, tag) | ~400 |
|
|
| Middleware auth | ~50 |
|
|
| Tag indexer | ~100 |
|
|
| CLI client | ~300 |
|
|
| CLI commands | ~400 |
|
|
| **Total** | ~1850 |
|