Implement SimpleNote Web API - full REST API with Express

- Express server with CORS, JSON middleware
- Auth middleware (Bearer token)
- Document CRUD with markdown storage
- Library CRUD with nested support
- Tag indexing and search
- Error handler middleware
- Config from env vars
- Init script for data structure
This commit is contained in:
Erwin
2026-03-28 03:27:27 +00:00
parent 0e244d2b30
commit 825dfba2a7
22 changed files with 2864 additions and 20 deletions

35
src/utils/errors.js Normal file
View File

@@ -0,0 +1,35 @@
/**
* SimpleNote Web - Custom Errors
*/
export class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message);
this.statusCode = statusCode;
this.code = code;
this.name = 'AppError';
}
}
export class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
this.name = 'NotFoundError';
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401, 'UNAUTHORIZED');
this.name = 'UnauthorizedError';
}
}
export class ValidationError extends AppError {
constructor(message) {
super(message, 400, 'VALIDATION_ERROR');
this.name = 'ValidationError';
}
}
export default { AppError, NotFoundError, UnauthorizedError, ValidationError };

58
src/utils/fsHelper.js Normal file
View File

@@ -0,0 +1,58 @@
/**
* SimpleNote Web - Filesystem Helper
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync } from 'fs';
import { join, resolve, relative, sep } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
export function ensureDir(dirPath) {
if (!existsSync(dirPath)) {
mkdirSync(dirPath, { recursive: true });
}
}
export function readJSON(filePath) {
if (!existsSync(filePath)) return null;
const content = readFileSync(filePath, 'utf-8');
return JSON.parse(content);
}
export function writeJSON(filePath, data) {
ensureDir(dirname(filePath));
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
}
export function deletePath(path) {
if (existsSync(path)) {
rmSync(path, { recursive: true, force: true });
}
}
export function pathExists(path) {
return existsSync(path);
}
export function listDir(dirPath) {
if (!existsSync(dirPath)) return [];
return readdirSync(dirPath, 'utf-8');
}
export function isDirectory(path) {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}
export function resolveSafe(base, target) {
const resolved = resolve(base, target);
if (!resolved.startsWith(resolve(base))) {
throw new Error('Path traversal detected');
}
return resolved;
}
export default { ensureDir, readJSON, writeJSON, deletePath, pathExists, listDir, isDirectory, resolveSafe };

68
src/utils/markdown.js Normal file
View File

@@ -0,0 +1,68 @@
/**
* SimpleNote Web - Markdown Utilities
* Helpers for parsing frontmatter and serializing markdown documents
*/
import matter from 'gray-matter';
import { generateId } from './uuid.js';
const VALID_TYPES = ['requirement', 'note', 'spec', 'general'];
const VALID_STATUSES = ['draft', 'approved', 'implemented'];
const VALID_PRIORITIES = ['high', 'medium', 'low'];
export function parseMarkdown(content) {
try {
const { data, content: body } = matter(content);
return {
metadata: {
id: data.id || null,
title: data.title || 'Untitled',
type: VALID_TYPES.includes(data.type) ? data.type : 'general',
status: VALID_STATUSES.includes(data.status) ? data.status : 'draft',
priority: VALID_PRIORITIES.includes(data.priority) ? data.priority : 'medium',
tags: Array.isArray(data.tags) ? data.tags : [],
createdBy: data.createdBy || null,
createdAt: data.createdAt || new Date().toISOString(),
},
body: body.trim(),
};
} catch (err) {
return {
metadata: {
id: null,
title: 'Untitled',
type: 'general',
status: 'draft',
priority: 'medium',
tags: [],
createdBy: null,
createdAt: new Date().toISOString(),
},
body: content,
};
}
}
export function serializeMarkdown(metadata, body = '') {
const frontmatter = [
'---',
`id: ${metadata.id || generateId()}`,
`title: ${metadata.title || 'Untitled'}`,
`type: ${metadata.type || 'general'}`,
`priority: ${metadata.priority || 'medium'}`,
`status: ${metadata.status || 'draft'}`,
`tags: [${(metadata.tags || []).join(', ')}]`,
metadata.createdBy ? `createdBy: ${metadata.createdBy}` : null,
`createdAt: ${metadata.createdAt || new Date().toISOString().split('T')[0]}`,
'---',
].filter(Boolean).join('\n');
return `${frontmatter}\n\n${body}`;
}
export function buildDefaultContent(title, type = 'general') {
const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
return `# ${title}\n\n## Descripción\nDescripción del ${typeLabel}.\n\n## Criterios de Aceptación\n- [ ] Criterio 1\n- [ ] Criterio 2\n`;
}
export default { parseMarkdown, serializeMarkdown, buildDefaultContent };

11
src/utils/uuid.js Normal file
View File

@@ -0,0 +1,11 @@
/**
* SimpleNote Web - UUID Helper
*/
import { v4 as uuidv4 } from 'uuid';
export function generateId() {
return uuidv4();
}
export default { generateId };