- Deleted the DebrosFramework integration test file to streamline the test suite. - Updated blog API server to import reflect-metadata for decorator support. - Changed Docker Compose command for blog integration tests to run real tests. - Modified TypeScript configuration for Docker to target ES2022 and enable synthetic default imports. - Removed obsolete Jest configuration and setup files for blog scenario tests. - Cleaned up global test setup by removing console mocks and custom matchers.
691 lines
18 KiB
JavaScript
691 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Import reflect-metadata first for decorator support
|
|
import 'reflect-metadata';
|
|
|
|
// Polyfill CustomEvent for Node.js environment
|
|
if (typeof globalThis.CustomEvent === 'undefined') {
|
|
globalThis.CustomEvent = class CustomEvent<T = any> extends Event {
|
|
detail: T;
|
|
|
|
constructor(type: string, eventInitDict?: CustomEventInit<T>) {
|
|
super(type, eventInitDict);
|
|
this.detail = eventInitDict?.detail;
|
|
}
|
|
} as any;
|
|
}
|
|
|
|
import express from 'express';
|
|
import { DebrosFramework } from '../../../../src/framework/DebrosFramework';
|
|
import { User, UserProfile, Category, Post, Comment } from '../models/BlogModels';
|
|
import { BlogValidation, ValidationError } from '../models/BlogValidation';
|
|
|
|
class BlogAPIServer {
|
|
private app: express.Application;
|
|
private framework: DebrosFramework;
|
|
private nodeId: string;
|
|
|
|
constructor() {
|
|
this.app = express();
|
|
this.nodeId = process.env.NODE_ID || 'blog-node';
|
|
this.setupMiddleware();
|
|
this.setupRoutes();
|
|
}
|
|
|
|
private setupMiddleware() {
|
|
this.app.use(express.json({ limit: '10mb' }));
|
|
this.app.use(express.urlencoded({ extended: true }));
|
|
|
|
// CORS
|
|
this.app.use((req, res, next) => {
|
|
res.header('Access-Control-Allow-Origin', '*');
|
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
if (req.method === 'OPTIONS') {
|
|
res.sendStatus(200);
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
|
|
// Logging
|
|
this.app.use((req, res, next) => {
|
|
console.log(`[${this.nodeId}] ${new Date().toISOString()} ${req.method} ${req.path}`);
|
|
next();
|
|
});
|
|
|
|
// Error handling
|
|
this.app.use(
|
|
(error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
console.error(`[${this.nodeId}] Error:`, error);
|
|
|
|
if (error instanceof ValidationError) {
|
|
return res.status(400).json({
|
|
error: error.message,
|
|
field: error.field,
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
nodeId: this.nodeId,
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
private setupRoutes() {
|
|
// Health check
|
|
this.app.get('/health', async (req, res) => {
|
|
try {
|
|
const peers = await this.getConnectedPeerCount();
|
|
res.json({
|
|
status: 'healthy',
|
|
nodeId: this.nodeId,
|
|
peers,
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
status: 'unhealthy',
|
|
nodeId: this.nodeId,
|
|
error: error.message,
|
|
});
|
|
}
|
|
});
|
|
|
|
// API routes
|
|
this.setupUserRoutes();
|
|
this.setupCategoryRoutes();
|
|
this.setupPostRoutes();
|
|
this.setupCommentRoutes();
|
|
this.setupMetricsRoutes();
|
|
}
|
|
|
|
private setupUserRoutes() {
|
|
// Create user
|
|
this.app.post('/api/users', async (req, res, next) => {
|
|
try {
|
|
const sanitizedData = BlogValidation.sanitizeUserInput(req.body);
|
|
BlogValidation.validateUser(sanitizedData);
|
|
|
|
const user = await User.create(sanitizedData);
|
|
|
|
console.log(`[${this.nodeId}] Created user: ${user.username} (${user.id})`);
|
|
res.status(201).json(user.toJSON());
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get user by ID
|
|
this.app.get('/api/users/:id', async (req, res, next) => {
|
|
try {
|
|
const user = await User.findById(req.params.id);
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
error: 'User not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
res.json(user.toJSON());
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get all users
|
|
this.app.get('/api/users', async (req, res, next) => {
|
|
try {
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
|
const search = req.query.search as string;
|
|
|
|
let query = User.query();
|
|
|
|
if (search) {
|
|
query = query
|
|
.where('username', 'like', `%${search}%`)
|
|
.orWhere('displayName', 'like', `%${search}%`);
|
|
}
|
|
|
|
const users = await query
|
|
.orderBy('createdAt', 'desc')
|
|
.limit(limit)
|
|
.offset((page - 1) * limit)
|
|
.find();
|
|
|
|
res.json({
|
|
users: users.map((u) => u.toJSON()),
|
|
page,
|
|
limit,
|
|
nodeId: this.nodeId,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Update user
|
|
this.app.put('/api/users/:id', async (req, res, next) => {
|
|
try {
|
|
const user = await User.findById(req.params.id);
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
error: 'User not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
// Only allow updating certain fields
|
|
const allowedFields = ['displayName', 'avatar', 'roles'];
|
|
const updateData: any = {};
|
|
|
|
allowedFields.forEach((field) => {
|
|
if (req.body[field] !== undefined) {
|
|
updateData[field] = req.body[field];
|
|
}
|
|
});
|
|
|
|
Object.assign(user, updateData);
|
|
await user.save();
|
|
|
|
console.log(`[${this.nodeId}] Updated user: ${user.username}`);
|
|
res.json(user.toJSON());
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// User login (update last login)
|
|
this.app.post('/api/users/:id/login', async (req, res, next) => {
|
|
try {
|
|
const user = await User.findById(req.params.id);
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
error: 'User not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
await user.updateLastLogin();
|
|
res.json({ message: 'Login recorded', lastLoginAt: user.lastLoginAt });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
private setupCategoryRoutes() {
|
|
// Create category
|
|
this.app.post('/api/categories', async (req, res, next) => {
|
|
try {
|
|
const sanitizedData = BlogValidation.sanitizeCategoryInput(req.body);
|
|
BlogValidation.validateCategory(sanitizedData);
|
|
|
|
const category = await Category.create(sanitizedData);
|
|
|
|
console.log(`[${this.nodeId}] Created category: ${category.name} (${category.id})`);
|
|
res.status(201).json(category);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get all categories
|
|
this.app.get('/api/categories', async (req, res, next) => {
|
|
try {
|
|
const categories = await Category.query()
|
|
.where('isActive', true)
|
|
.orderBy('name', 'asc')
|
|
.find();
|
|
|
|
res.json({
|
|
categories,
|
|
nodeId: this.nodeId,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get category by ID
|
|
this.app.get('/api/categories/:id', async (req, res, next) => {
|
|
try {
|
|
const category = await Category.findById(req.params.id);
|
|
if (!category) {
|
|
return res.status(404).json({
|
|
error: 'Category not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
res.json(category);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
private setupPostRoutes() {
|
|
// Create post
|
|
this.app.post('/api/posts', async (req, res, next) => {
|
|
try {
|
|
const sanitizedData = BlogValidation.sanitizePostInput(req.body);
|
|
BlogValidation.validatePost(sanitizedData);
|
|
|
|
const post = await Post.create(sanitizedData);
|
|
|
|
console.log(`[${this.nodeId}] Created post: ${post.title} (${post.id})`);
|
|
res.status(201).json(post);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get post by ID with relationships
|
|
this.app.get('/api/posts/:id', async (req, res, next) => {
|
|
try {
|
|
const post = await Post.query()
|
|
.where('id', req.params.id)
|
|
.with(['author', 'category', 'comments'])
|
|
.first();
|
|
|
|
if (!post) {
|
|
return res.status(404).json({
|
|
error: 'Post not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
res.json(post);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get all posts with pagination and filters
|
|
this.app.get('/api/posts', async (req, res, next) => {
|
|
try {
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const limit = Math.min(parseInt(req.query.limit as string) || 10, 50);
|
|
const status = req.query.status as string;
|
|
const authorId = req.query.authorId as string;
|
|
const categoryId = req.query.categoryId as string;
|
|
const tag = req.query.tag as string;
|
|
|
|
let query = Post.query().with(['author', 'category']);
|
|
|
|
if (status) {
|
|
query = query.where('status', status);
|
|
}
|
|
|
|
if (authorId) {
|
|
query = query.where('authorId', authorId);
|
|
}
|
|
|
|
if (categoryId) {
|
|
query = query.where('categoryId', categoryId);
|
|
}
|
|
|
|
if (tag) {
|
|
query = query.where('tags', 'includes', tag);
|
|
}
|
|
|
|
const posts = await query
|
|
.orderBy('createdAt', 'desc')
|
|
.limit(limit)
|
|
.offset((page - 1) * limit)
|
|
.find();
|
|
|
|
res.json({
|
|
posts,
|
|
page,
|
|
limit,
|
|
nodeId: this.nodeId,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Update post
|
|
this.app.put('/api/posts/:id', async (req, res, next) => {
|
|
try {
|
|
const post = await Post.findById(req.params.id);
|
|
if (!post) {
|
|
return res.status(404).json({
|
|
error: 'Post not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
BlogValidation.validatePostUpdate(req.body);
|
|
|
|
Object.assign(post, req.body);
|
|
post.updatedAt = Date.now();
|
|
await post.save();
|
|
|
|
console.log(`[${this.nodeId}] Updated post: ${post.title}`);
|
|
res.json(post);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Publish post
|
|
this.app.post('/api/posts/:id/publish', async (req, res, next) => {
|
|
try {
|
|
const post = await Post.findById(req.params.id);
|
|
if (!post) {
|
|
return res.status(404).json({
|
|
error: 'Post not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
await post.publish();
|
|
console.log(`[${this.nodeId}] Published post: ${post.title}`);
|
|
res.json(post);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Unpublish post
|
|
this.app.post('/api/posts/:id/unpublish', async (req, res, next) => {
|
|
try {
|
|
const post = await Post.findById(req.params.id);
|
|
if (!post) {
|
|
return res.status(404).json({
|
|
error: 'Post not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
await post.unpublish();
|
|
console.log(`[${this.nodeId}] Unpublished post: ${post.title}`);
|
|
res.json(post);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Like post
|
|
this.app.post('/api/posts/:id/like', async (req, res, next) => {
|
|
try {
|
|
const post = await Post.findById(req.params.id);
|
|
if (!post) {
|
|
return res.status(404).json({
|
|
error: 'Post not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
await post.like();
|
|
res.json({ likeCount: post.likeCount });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// View post (increment view count)
|
|
this.app.post('/api/posts/:id/view', async (req, res, next) => {
|
|
try {
|
|
const post = await Post.findById(req.params.id);
|
|
if (!post) {
|
|
return res.status(404).json({
|
|
error: 'Post not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
await post.incrementViews();
|
|
res.json({ viewCount: post.viewCount });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
private setupCommentRoutes() {
|
|
// Create comment
|
|
this.app.post('/api/comments', async (req, res, next) => {
|
|
try {
|
|
const sanitizedData = BlogValidation.sanitizeCommentInput(req.body);
|
|
BlogValidation.validateComment(sanitizedData);
|
|
|
|
const comment = await Comment.create(sanitizedData);
|
|
|
|
console.log(
|
|
`[${this.nodeId}] Created comment on post ${comment.postId} by ${comment.authorId}`,
|
|
);
|
|
res.status(201).json(comment);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get comments for a post
|
|
this.app.get('/api/posts/:postId/comments', async (req, res, next) => {
|
|
try {
|
|
const comments = await Comment.query()
|
|
.where('postId', req.params.postId)
|
|
.where('isApproved', true)
|
|
.with(['author'])
|
|
.orderBy('createdAt', 'asc')
|
|
.find();
|
|
|
|
res.json({
|
|
comments,
|
|
nodeId: this.nodeId,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Approve comment
|
|
this.app.post('/api/comments/:id/approve', async (req, res, next) => {
|
|
try {
|
|
const comment = await Comment.findById(req.params.id);
|
|
if (!comment) {
|
|
return res.status(404).json({
|
|
error: 'Comment not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
await comment.approve();
|
|
console.log(`[${this.nodeId}] Approved comment ${comment.id}`);
|
|
res.json(comment);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Like comment
|
|
this.app.post('/api/comments/:id/like', async (req, res, next) => {
|
|
try {
|
|
const comment = await Comment.findById(req.params.id);
|
|
if (!comment) {
|
|
return res.status(404).json({
|
|
error: 'Comment not found',
|
|
nodeId: this.nodeId,
|
|
});
|
|
}
|
|
|
|
await comment.like();
|
|
res.json({ likeCount: comment.likeCount });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
private setupMetricsRoutes() {
|
|
// Network metrics
|
|
this.app.get('/api/metrics/network', async (req, res, next) => {
|
|
try {
|
|
const peers = await this.getConnectedPeerCount();
|
|
res.json({
|
|
nodeId: this.nodeId,
|
|
peers,
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Data metrics
|
|
this.app.get('/api/metrics/data', async (req, res, next) => {
|
|
try {
|
|
const [userCount, postCount, commentCount, categoryCount] = await Promise.all([
|
|
User.count(),
|
|
Post.count(),
|
|
Comment.count(),
|
|
Category.count(),
|
|
]);
|
|
|
|
res.json({
|
|
nodeId: this.nodeId,
|
|
counts: {
|
|
users: userCount,
|
|
posts: postCount,
|
|
comments: commentCount,
|
|
categories: categoryCount,
|
|
},
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Framework metrics
|
|
this.app.get('/api/metrics/framework', async (req, res, next) => {
|
|
try {
|
|
const metrics = this.framework.getMetrics();
|
|
res.json({
|
|
nodeId: this.nodeId,
|
|
...metrics,
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
private async getConnectedPeerCount(): Promise<number> {
|
|
try {
|
|
if (this.framework) {
|
|
const ipfsService = this.framework.getIPFSService();
|
|
if (ipfsService && ipfsService.getConnectedPeers) {
|
|
const peers = await ipfsService.getConnectedPeers();
|
|
return peers.size;
|
|
}
|
|
}
|
|
return 0;
|
|
} catch (error) {
|
|
console.warn(`[${this.nodeId}] Failed to get peer count:`, error.message);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
async start() {
|
|
try {
|
|
console.log(`[${this.nodeId}] Starting Blog API Server...`);
|
|
|
|
// Wait for dependencies
|
|
await this.waitForDependencies();
|
|
|
|
// Initialize framework
|
|
await this.initializeFramework();
|
|
|
|
// Start HTTP server
|
|
const port = process.env.NODE_PORT || 3000;
|
|
this.app.listen(port, () => {
|
|
console.log(`[${this.nodeId}] Blog API server listening on port ${port}`);
|
|
console.log(`[${this.nodeId}] Health check: http://localhost:${port}/health`);
|
|
});
|
|
} catch (error) {
|
|
console.error(`[${this.nodeId}] Failed to start:`, error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
private async waitForDependencies(): Promise<void> {
|
|
// In a real deployment, you might wait for database connections, etc.
|
|
console.log(`[${this.nodeId}] Dependencies ready`);
|
|
}
|
|
|
|
private async initializeFramework(): Promise<void> {
|
|
// Import services
|
|
const { IPFSService } = await import('../../../../src/framework/services/IPFSService');
|
|
const { OrbitDBService } = await import(
|
|
'../../../../src/framework/services/RealOrbitDBService'
|
|
);
|
|
const { FrameworkIPFSService, FrameworkOrbitDBService } = await import(
|
|
'../../../../src/framework/services/OrbitDBService'
|
|
);
|
|
|
|
// Initialize IPFS service
|
|
const ipfsService = new IPFSService({
|
|
swarmKeyFile: process.env.SWARM_KEY_FILE,
|
|
bootstrap: process.env.BOOTSTRAP_PEER ? [`/ip4/${process.env.BOOTSTRAP_PEER}/tcp/4001`] : [],
|
|
ports: {
|
|
swarm: parseInt(process.env.IPFS_PORT) || 4001,
|
|
},
|
|
});
|
|
|
|
await ipfsService.init();
|
|
console.log(`[${this.nodeId}] IPFS service initialized`);
|
|
|
|
// Initialize OrbitDB service
|
|
const orbitDBService = new OrbitDBService(ipfsService);
|
|
await orbitDBService.init();
|
|
console.log(`[${this.nodeId}] OrbitDB service initialized`);
|
|
|
|
// Wrap services for framework
|
|
const frameworkIPFS = new FrameworkIPFSService(ipfsService);
|
|
const frameworkOrbitDB = new FrameworkOrbitDBService(orbitDBService);
|
|
|
|
// Initialize framework
|
|
this.framework = new DebrosFramework({
|
|
environment: 'test',
|
|
features: {
|
|
autoMigration: true,
|
|
automaticPinning: true,
|
|
pubsub: true,
|
|
queryCache: true,
|
|
relationshipCache: true,
|
|
},
|
|
performance: {
|
|
queryTimeout: 10000,
|
|
maxConcurrentOperations: 20,
|
|
batchSize: 50,
|
|
},
|
|
});
|
|
|
|
await this.framework.initialize(orbitDBService, ipfsService);
|
|
console.log(`[${this.nodeId}] DebrosFramework initialized successfully`);
|
|
}
|
|
}
|
|
|
|
// Handle graceful shutdown
|
|
process.on('SIGTERM', () => {
|
|
console.log('Received SIGTERM, shutting down gracefully...');
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on('SIGINT', () => {
|
|
console.log('Received SIGINT, shutting down gracefully...');
|
|
process.exit(0);
|
|
});
|
|
|
|
// Start the server
|
|
const server = new BlogAPIServer();
|
|
server.start();
|