veza/fixtures/services/chat-server/index.ts
2025-12-03 22:56:50 +01:00

492 lines
No EOL
14 KiB
TypeScript

import { Client } from 'pg'
import { createClient } from 'redis'
import { DataRelationManager } from '../../core/utils/data-relations'
import { globalConfig } from '../../core/config'
import type { User, Conversation, Message } from '../../core/schemas/database'
/**
* Chat server fixtures - Database seeding and Redis cache
*/
export class ChatServerFixtures {
private static dbClient: Client | null = null
private static redisClient: any = null
/**
* Initialize database connections
*/
static async initialize(): Promise<void> {
console.log('💬 Initializing chat server fixtures...')
// Initialize PostgreSQL connection
this.dbClient = new Client({
host: globalConfig.database.host,
port: globalConfig.database.port,
user: globalConfig.database.username,
password: globalConfig.database.password,
database: globalConfig.database.databases.chat,
})
try {
await this.dbClient.connect()
console.log('✅ Connected to PostgreSQL (chat)')
} catch (error) {
console.warn('⚠️ Could not connect to PostgreSQL (chat):', error)
this.dbClient = null
}
// Initialize Redis connection
this.redisClient = createClient({
socket: {
host: globalConfig.redis.host,
port: globalConfig.redis.port,
},
password: globalConfig.redis.password || undefined,
})
try {
await this.redisClient.connect()
console.log('✅ Connected to Redis')
} catch (error) {
console.warn('⚠️ Could not connect to Redis:', error)
this.redisClient = null
}
}
/**
* Seed PostgreSQL database with chat data
*/
static async seedDatabase(): Promise<void> {
if (!this.dbClient) {
console.warn('⚠️ No database connection available')
return
}
console.log('🗄️ Seeding chat database...')
try {
// Begin transaction
await this.dbClient.query('BEGIN')
// Clear existing data
await this.clearDatabase()
// Get data from relation manager
const allData = DataRelationManager.getAll()
const users = Array.from(allData.users.values())
const conversations = Array.from(allData.conversations.values())
const messages = Array.from(allData.messages.values())
// Seed users
console.log(`📝 Seeding ${users.length} users...`)
await this.seedUsers(users)
// Seed conversations
console.log(`📝 Seeding ${conversations.length} conversations...`)
await this.seedConversations(conversations)
// Seed messages
console.log(`📝 Seeding ${messages.length} messages...`)
await this.seedMessages(messages)
// Commit transaction
await this.dbClient.query('COMMIT')
console.log('✅ Chat database seeded successfully')
} catch (error) {
await this.dbClient.query('ROLLBACK')
console.error('❌ Error seeding chat database:', error)
throw error
}
}
/**
* Seed Redis cache with session data
*/
static async seedRedis(): Promise<void> {
if (!this.redisClient) {
console.warn('⚠️ No Redis connection available')
return
}
console.log('🔄 Seeding Redis cache...')
try {
// Clear existing cache
await this.redisClient.flushDb()
const allData = DataRelationManager.getAll()
const users = Array.from(allData.users.values())
const conversations = Array.from(allData.conversations.values())
// Cache active users
const activeUsers = users.filter(user => user.status === 'active')
for (const user of activeUsers) {
const sessionData = {
userId: user.id,
username: user.username,
status: 'online',
lastSeen: new Date().toISOString(),
socketId: `socket_${user.id}_${Date.now()}`,
}
await this.redisClient.setEx(
`session:${user.id}`,
3600, // 1 hour TTL
JSON.stringify(sessionData)
)
// Add to online users set
await this.redisClient.sAdd('users:online', user.id)
}
// Cache conversation metadata
for (const conversation of conversations) {
const metadata = {
id: conversation.id,
name: conversation.name,
type: conversation.type,
participantCount: conversation.participantIds.length,
lastActivity: conversation.lastActivityAt.toISOString(),
}
await this.redisClient.setEx(
`conversation:${conversation.id}`,
1800, // 30 minutes TTL
JSON.stringify(metadata)
)
// Cache participant lists
await this.redisClient.sAdd(
`conversation:${conversation.id}:participants`,
...conversation.participantIds
)
// Set expiry for participant sets
await this.redisClient.expire(
`conversation:${conversation.id}:participants`,
1800
)
}
// Cache typing indicators (empty initially)
await this.redisClient.set('typing:indicators', '{}')
// Cache message delivery status
const recentMessages = Array.from(allData.messages.values())
.filter(msg => {
const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
return msg.createdAt > dayAgo
})
for (const message of recentMessages) {
await this.redisClient.setEx(
`message:${message.id}:status`,
86400, // 24 hours TTL
message.status
)
}
console.log(`✅ Redis cache seeded with ${activeUsers.length} sessions and ${conversations.length} conversations`)
} catch (error) {
console.error('❌ Error seeding Redis cache:', error)
throw error
}
}
/**
* Generate WebSocket event data
*/
static generateWebSocketEvents(): any[] {
const allData = DataRelationManager.getAll()
const users = Array.from(allData.users.values())
const conversations = Array.from(allData.conversations.values())
const messages = Array.from(allData.messages.values())
const events: any[] = []
// User connection events
users.filter(u => u.status === 'active').forEach(user => {
events.push({
type: 'user_connected',
userId: user.id,
data: {
username: user.username,
status: 'online',
lastSeen: new Date().toISOString()
},
timestamp: new Date().toISOString()
})
})
// Recent message events
const recentMessages = messages
.filter(msg => {
const hourAgo = new Date(Date.now() - 60 * 60 * 1000)
return msg.createdAt > hourAgo
})
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
recentMessages.forEach(message => {
events.push({
type: 'message_sent',
conversationId: message.conversationId,
data: {
id: message.id,
senderId: message.senderId,
content: message.content,
type: message.type,
createdAt: message.createdAt.toISOString()
},
timestamp: message.createdAt.toISOString()
})
})
// Typing indicator events
conversations.forEach(conversation => {
const typingUser = conversation.participantIds[0]
events.push({
type: 'typing_start',
conversationId: conversation.id,
data: {
userId: typingUser,
isTyping: true
},
timestamp: new Date(Date.now() - Math.random() * 30000).toISOString()
})
})
return events.sort((a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
)
}
/**
* Get conversation statistics
*/
static async getConversationStats(): Promise<any> {
if (!this.dbClient) {
return this.getMockConversationStats()
}
try {
const result = await this.dbClient.query(`
SELECT
c.type,
COUNT(*) as count,
AVG(array_length(c.participant_ids, 1)) as avg_participants,
COUNT(m.id) as total_messages
FROM conversations c
LEFT JOIN messages m ON c.id = m.conversation_id
GROUP BY c.type
`)
return result.rows.reduce((stats, row) => {
stats[row.type] = {
count: parseInt(row.count),
avgParticipants: Math.round(parseFloat(row.avg_participants)),
totalMessages: parseInt(row.total_messages)
}
return stats
}, {})
} catch (error) {
console.error('Error getting conversation stats:', error)
return this.getMockConversationStats()
}
}
/**
* Private helper methods
*/
private static async clearDatabase(): Promise<void> {
if (!this.dbClient) return
const tables = ['messages', 'conversation_members', 'conversations', 'users']
for (const table of tables) {
await this.dbClient.query(`DELETE FROM ${table}`)
}
console.log('🗑️ Cleared existing chat database data')
}
private static async seedUsers(users: User[]): Promise<void> {
if (!this.dbClient) return
const query = `
INSERT INTO users (
id, username, email, display_name, avatar_url,
is_active, last_seen, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
for (const user of users) {
await this.dbClient.query(query, [
user.id,
user.username,
user.email,
user.displayName || `${user.firstName} ${user.lastName}`,
user.avatar,
user.status === 'active',
user.lastSeenAt,
user.createdAt,
user.updatedAt
])
}
}
private static async seedConversations(conversations: Conversation[]): Promise<void> {
if (!this.dbClient) return
const conversationQuery = `
INSERT INTO conversations (
id, name, description, conversation_type, is_private,
created_by, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
const memberQuery = `
INSERT INTO conversation_members (conversation_id, user_id, role, joined_at)
VALUES ($1, $2, $3, $4)
`
for (const conversation of conversations) {
// Insert conversation
await this.dbClient.query(conversationQuery, [
conversation.id,
conversation.name,
conversation.description,
conversation.type,
conversation.isPrivate,
conversation.createdById,
conversation.createdAt,
conversation.updatedAt
])
// Insert participants
for (const participantId of conversation.participantIds) {
const role = participantId === conversation.createdById ? 'admin' : 'member'
await this.dbClient.query(memberQuery, [
conversation.id,
participantId,
role,
conversation.createdAt
])
}
}
}
private static async seedMessages(messages: Message[]): Promise<void> {
if (!this.dbClient) return
const query = `
INSERT INTO messages (
id, conversation_id, sender_id, content, message_type,
parent_message_id, is_pinned, is_deleted, created_at, updated_at, status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
for (const message of messages) {
await this.dbClient.query(query, [
message.id,
message.conversationId,
message.senderId,
message.content,
message.type,
message.parentMessageId,
false, // is_pinned
message.isDeleted,
message.createdAt,
message.updatedAt,
message.status
])
}
}
private static getMockConversationStats(): any {
const stats = DataRelationManager.getStatistics()
return {
direct: {
count: Math.floor(stats.conversations * 0.6),
avgParticipants: 2,
totalMessages: Math.floor(stats.messages * 0.4)
},
group: {
count: Math.floor(stats.conversations * 0.3),
avgParticipants: 6,
totalMessages: Math.floor(stats.messages * 0.4)
},
channel: {
count: Math.floor(stats.conversations * 0.1),
avgParticipants: 25,
totalMessages: Math.floor(stats.messages * 0.2)
}
}
}
/**
* Cleanup methods
*/
static async cleanup(): Promise<void> {
if (this.dbClient) {
await this.dbClient.end()
this.dbClient = null
}
if (this.redisClient) {
await this.redisClient.quit()
this.redisClient = null
}
console.log('🧹 Chat server fixtures cleanup completed')
}
/**
* Health check
*/
static async healthCheck(): Promise<{
database: boolean
redis: boolean
dataConsistency: boolean
}> {
let database = false
let redis = false
let dataConsistency = false
// Check database connection
if (this.dbClient) {
try {
await this.dbClient.query('SELECT 1')
database = true
} catch (error) {
console.error('Database health check failed:', error)
}
}
// Check Redis connection
if (this.redisClient) {
try {
await this.redisClient.ping()
redis = true
} catch (error) {
console.error('Redis health check failed:', error)
}
}
// Check data consistency
try {
const validation = DataRelationManager.validateRelations()
dataConsistency = validation.isValid
if (!validation.isValid) {
console.warn('Data consistency issues:', validation.errors)
}
} catch (error) {
console.error('Data consistency check failed:', error)
}
return { database, redis, dataConsistency }
}
}