tractatus/src/controllers/documents.controller.js
TheFlow 0d75492c60 feat: add API routes, controllers, and migration tools
Implemented complete backend API foundation with authentication, document
management, blog operations, and admin functionality. Added migration tools
for database seeding and document import.

**Controllers (4 files):**
- auth.controller.js: User authentication (login, getCurrentUser, logout)
- documents.controller.js: Document CRUD operations
- blog.controller.js: Blog post management with admin/public access
- admin.controller.js: Admin dashboard (stats, moderation queue, activity)

**Routes (5 files):**
- auth.routes.js: Authentication endpoints
- documents.routes.js: Document API endpoints
- blog.routes.js: Blog API endpoints
- admin.routes.js: Admin API endpoints
- index.js: Central routing configuration with API documentation

**Migration Tools (2 scripts):**
- seed-admin.js: Create admin user for system access
- migrate-documents.js: Import markdown documents with metadata extraction,
  slug generation, and dry-run support. Successfully migrated 8 documents
  from anthropic-submission directory.

**Server Updates:**
- Integrated all API routes under /api namespace
- Updated homepage to reflect completed API implementation
- Maintained security middleware (Helmet, CORS, rate limiting)

**Testing:**
 Server starts successfully on port 9000
 Authentication flow working (login, token validation)
 Document endpoints tested (list, get by slug)
 Admin stats endpoint verified (requires authentication)
 Migration completed: 8 documents imported

**Database Status:**
- Documents collection: 8 technical papers
- Users collection: 1 admin user
- All indexes operational

This completes the core backend API infrastructure. Next steps: build
Tractatus governance services (InstructionClassifier, CrossReferenceValidator,
BoundaryEnforcer).

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 00:36:40 +13:00

265 lines
5.7 KiB
JavaScript

/**
* Documents Controller
* Handles framework documentation CRUD operations
*/
const Document = require('../models/Document.model');
const { markdownToHtml, extractTOC } = require('../utils/markdown.util');
const logger = require('../utils/logger.util');
/**
* List all documents
* GET /api/documents
*/
async function listDocuments(req, res) {
try {
const { limit = 50, skip = 0, quadrant } = req.query;
let documents;
let total;
if (quadrant) {
documents = await Document.findByQuadrant(quadrant, {
limit: parseInt(limit),
skip: parseInt(skip)
});
total = await Document.count({ quadrant });
} else {
documents = await Document.list({
limit: parseInt(limit),
skip: parseInt(skip)
});
total = await Document.count();
}
res.json({
success: true,
documents,
pagination: {
total,
limit: parseInt(limit),
skip: parseInt(skip),
hasMore: parseInt(skip) + documents.length < total
}
});
} catch (error) {
logger.error('List documents error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Get document by ID or slug
* GET /api/documents/:identifier
*/
async function getDocument(req, res) {
try {
const { identifier } = req.params;
// Try to find by ID first, then by slug
let document;
if (identifier.match(/^[0-9a-fA-F]{24}$/)) {
document = await Document.findById(identifier);
} else {
document = await Document.findBySlug(identifier);
}
if (!document) {
return res.status(404).json({
error: 'Not Found',
message: 'Document not found'
});
}
res.json({
success: true,
document
});
} catch (error) {
logger.error('Get document error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Search documents
* GET /api/documents/search
*/
async function searchDocuments(req, res) {
try {
const { q, limit = 20, skip = 0 } = req.query;
if (!q) {
return res.status(400).json({
error: 'Bad Request',
message: 'Search query (q) is required'
});
}
const documents = await Document.search(q, {
limit: parseInt(limit),
skip: parseInt(skip)
});
res.json({
success: true,
query: q,
documents,
count: documents.length
});
} catch (error) {
logger.error('Search documents error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Create document (admin only)
* POST /api/documents
*/
async function createDocument(req, res) {
try {
const { title, slug, quadrant, persistence, content_markdown, metadata } = req.body;
// Convert markdown to HTML
const content_html = markdownToHtml(content_markdown);
// Extract table of contents
const toc = extractTOC(content_markdown);
// Create search index from content
const search_index = `${title} ${content_markdown}`.toLowerCase();
const document = await Document.create({
title,
slug,
quadrant,
persistence,
content_html,
content_markdown,
toc,
metadata,
search_index
});
logger.info(`Document created: ${slug} by ${req.user.email}`);
res.status(201).json({
success: true,
document
});
} catch (error) {
logger.error('Create document error:', error);
// Handle duplicate slug
if (error.code === 11000) {
return res.status(409).json({
error: 'Conflict',
message: 'A document with this slug already exists'
});
}
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Update document (admin only)
* PUT /api/documents/:id
*/
async function updateDocument(req, res) {
try {
const { id } = req.params;
const updates = { ...req.body };
// If content_markdown is updated, regenerate HTML and TOC
if (updates.content_markdown) {
updates.content_html = markdownToHtml(updates.content_markdown);
updates.toc = extractTOC(updates.content_markdown);
updates.search_index = `${updates.title || ''} ${updates.content_markdown}`.toLowerCase();
}
const success = await Document.update(id, updates);
if (!success) {
return res.status(404).json({
error: 'Not Found',
message: 'Document not found'
});
}
const document = await Document.findById(id);
logger.info(`Document updated: ${id} by ${req.user.email}`);
res.json({
success: true,
document
});
} catch (error) {
logger.error('Update document error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Delete document (admin only)
* DELETE /api/documents/:id
*/
async function deleteDocument(req, res) {
try {
const { id } = req.params;
const success = await Document.delete(id);
if (!success) {
return res.status(404).json({
error: 'Not Found',
message: 'Document not found'
});
}
logger.info(`Document deleted: ${id} by ${req.user.email}`);
res.json({
success: true,
message: 'Document deleted successfully'
});
} catch (error) {
logger.error('Delete document error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
module.exports = {
listDocuments,
getDocument,
searchDocuments,
createDocument,
updateDocument,
deleteDocument
};